Move revoke extension into core

Remove revoke as an extension and move it to a core resource.
For now we leave the database migrations in the extension directory
until we have a general policy for merging these into core.

DocImpact: update keystone-paste and remove revoke from pipeline

Change-Id: I2d6f425a508b7acb4b4d079e4387f25bf7555683
Implements: bp move-extensions
This commit is contained in:
Steve Martinelli 2015-10-15 22:21:11 -04:00 committed by Dave Chen
parent a83b6b29e8
commit 32b70d15c4
19 changed files with 279 additions and 185 deletions

View File

@ -39,9 +39,6 @@ use = egg:keystone#endpoint_filter_extension
[filter:simple_cert_extension]
use = egg:keystone#simple_cert_extension
[filter:revoke_extension]
use = egg:keystone#revoke_extension
[filter:url_normalize]
use = egg:keystone#url_normalize
@ -70,7 +67,7 @@ pipeline = sizelimit url_normalize request_id build_auth_context token_auth admi
[pipeline:api_v3]
# The last item in this pipeline must be service_v3 or an equivalent
# application. It cannot be a filter.
pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension endpoint_filter_extension service_v3
pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension endpoint_filter_extension service_v3
[app:public_version_service]
use = egg:keystone#public_version_service

View File

@ -1,13 +0,0 @@
# 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 keystone.contrib.revoke.core import * # noqa

View File

@ -10,65 +10,20 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from oslo_config import cfg
from oslo_log import versionutils
from oslo_utils import timeutils
from keystone.common import kvs
from keystone.contrib import revoke
from keystone import exception
from keystone.revoke.backends import kvs
CONF = cfg.CONF
_EVENT_KEY = 'os-revoke-events'
_KVS_BACKEND = 'openstack.kvs.Memory'
_OLD = 'keystone.contrib.revoke.backends.kvs.Revoke'
_NEW = 'kvs'
class Revoke(revoke.RevokeDriverV8):
class Revoke(kvs.Revoke):
@versionutils.deprecated(
versionutils.deprecated.JUNO,
in_favor_of='keystone.contrib.revoke.backends.sql',
remove_in=+1,
what='keystone.contrib.revoke.backends.kvs')
def __init__(self, **kwargs):
super(Revoke, self).__init__()
self._store = kvs.get_key_value_store('os-revoke-driver')
self._store.configure(backing_store=_KVS_BACKEND, **kwargs)
def _list_events(self):
try:
return self._store.get(_EVENT_KEY)
except exception.NotFound:
return []
def list_events(self, last_fetch=None):
results = []
with self._store.get_lock(_EVENT_KEY):
events = self._list_events()
for event in events:
revoked_at = event.revoked_at
if last_fetch is None or revoked_at > last_fetch:
results.append(event)
return results
def revoke(self, event):
pruned = []
expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
oldest = timeutils.utcnow() - expire_delta
with self._store.get_lock(_EVENT_KEY) as lock:
events = self._list_events()
if event:
events.append(event)
for event in events:
revoked_at = event.revoked_at
if revoked_at > oldest:
pruned.append(event)
self._store.set(_EVENT_KEY, pruned, lock)
@versionutils.deprecated(versionutils.deprecated.MITAKA,
in_favor_of=_NEW,
remove_in=2,
what=_OLD)
def __init__(self, *args, **kwargs):
super(Revoke, self).__init__(*args, **kwargs)

View File

@ -10,95 +10,19 @@
# License for the specific language governing permissions and limitations
# under the License.
import uuid
from oslo_log import versionutils
from keystone.common import sql
from keystone.contrib import revoke
from keystone.contrib.revoke import model
from keystone.revoke.backends import sql
class RevocationEvent(sql.ModelBase, sql.ModelDictMixin):
__tablename__ = 'revocation_event'
attributes = model.REVOKE_KEYS
# The id field is not going to be exposed to the outside world.
# It is, however, necessary for SQLAlchemy.
id = sql.Column(sql.String(64), primary_key=True)
domain_id = sql.Column(sql.String(64))
project_id = sql.Column(sql.String(64))
user_id = sql.Column(sql.String(64))
role_id = sql.Column(sql.String(64))
trust_id = sql.Column(sql.String(64))
consumer_id = sql.Column(sql.String(64))
access_token_id = sql.Column(sql.String(64))
issued_before = sql.Column(sql.DateTime(), nullable=False)
expires_at = sql.Column(sql.DateTime())
revoked_at = sql.Column(sql.DateTime(), nullable=False, index=True)
audit_id = sql.Column(sql.String(32))
audit_chain_id = sql.Column(sql.String(32))
_OLD = "keystone.contrib.revoke.backends.sql.Revoke"
_NEW = "sql"
class Revoke(revoke.RevokeDriverV8):
def _flush_batch_size(self, dialect):
batch_size = 0
if dialect == 'ibm_db_sa':
# This functionality is limited to DB2, because
# it is necessary to prevent the transaction log
# from filling up, whereas at least some of the
# other supported databases do not support update
# queries with LIMIT subqueries nor do they appear
# to require the use of such queries when deleting
# large numbers of records at once.
batch_size = 100
# Limit of 100 is known to not fill a transaction log
# of default maximum size while not significantly
# impacting the performance of large token purges on
# systems where the maximum transaction log size has
# been increased beyond the default.
return batch_size
class Revoke(sql.Revoke):
def _prune_expired_events(self):
oldest = revoke.revoked_before_cutoff_time()
session = sql.get_session()
dialect = session.bind.dialect.name
batch_size = self._flush_batch_size(dialect)
if batch_size > 0:
query = session.query(RevocationEvent.id)
query = query.filter(RevocationEvent.revoked_at < oldest)
query = query.limit(batch_size).subquery()
delete_query = (session.query(RevocationEvent).
filter(RevocationEvent.id.in_(query)))
while True:
rowcount = delete_query.delete(synchronize_session=False)
if rowcount == 0:
break
else:
query = session.query(RevocationEvent)
query = query.filter(RevocationEvent.revoked_at < oldest)
query.delete(synchronize_session=False)
session.flush()
def list_events(self, last_fetch=None):
session = sql.get_session()
query = session.query(RevocationEvent).order_by(
RevocationEvent.revoked_at)
if last_fetch:
query = query.filter(RevocationEvent.revoked_at > last_fetch)
events = [model.RevokeEvent(**e.to_dict()) for e in query]
return events
def revoke(self, event):
kwargs = dict()
for attr in model.REVOKE_KEYS:
kwargs[attr] = getattr(event, attr)
kwargs['id'] = uuid.uuid4().hex
record = RevocationEvent(**kwargs)
session = sql.get_session()
with session.begin():
session.add(record)
self._prune_expired_events()
@versionutils.deprecated(versionutils.deprecated.MITAKA,
in_favor_of=_NEW,
what=_OLD)
def __init__(self, *args, **kwargs):
super(Revoke, self).__init__(*args, **kwargs)

View File

@ -10,20 +10,22 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.common import json_home
from oslo_log import log
from oslo_log import versionutils
from keystone.common import wsgi
from keystone.contrib.revoke import controllers
from keystone.i18n import _
class RevokeExtension(wsgi.V3ExtensionRouter):
LOG = log.getLogger(__name__)
PATH_PREFIX = '/OS-REVOKE'
def add_routes(self, mapper):
revoke_controller = controllers.RevokeController()
self._add_resource(
mapper, revoke_controller,
path=self.PATH_PREFIX + '/events',
get_action='list_revoke_events',
rel=json_home.build_v3_extension_resource_relation(
'OS-REVOKE', '1.0', 'events'))
class RevokeExtension(wsgi.Middleware):
def __init__(self, *args, **kwargs):
super(RevokeExtension, self).__init__(*args, **kwargs)
msg = _("Remove revoke_extension from the paste pipeline, the "
"revoke extension is now always available. Update the "
"[pipeline:api_v3] section in keystone-paste.ini accordingly, "
"as it will be removed in the O release.")
versionutils.report_deprecated_feature(LOG, msg)

View File

@ -0,0 +1,14 @@
# 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 keystone.revoke.core import * # noqa
from keystone.revoke.routers import * # noqa

View File

View File

@ -0,0 +1,74 @@
# 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.
import datetime
from oslo_config import cfg
from oslo_log import versionutils
from oslo_utils import timeutils
from keystone.common import kvs
from keystone import exception
from keystone import revoke
CONF = cfg.CONF
_EVENT_KEY = 'os-revoke-events'
_KVS_BACKEND = 'openstack.kvs.Memory'
class Revoke(revoke.RevokeDriverV8):
@versionutils.deprecated(
versionutils.deprecated.JUNO,
in_favor_of='keystone.revoke.backends.sql',
remove_in=+1,
what='keystone.revoke.backends.kvs')
def __init__(self, **kwargs):
super(Revoke, self).__init__()
self._store = kvs.get_key_value_store('os-revoke-driver')
self._store.configure(backing_store=_KVS_BACKEND, **kwargs)
def _list_events(self):
try:
return self._store.get(_EVENT_KEY)
except exception.NotFound:
return []
def list_events(self, last_fetch=None):
results = []
with self._store.get_lock(_EVENT_KEY):
events = self._list_events()
for event in events:
revoked_at = event.revoked_at
if last_fetch is None or revoked_at > last_fetch:
results.append(event)
return results
def revoke(self, event):
pruned = []
expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
oldest = timeutils.utcnow() - expire_delta
with self._store.get_lock(_EVENT_KEY) as lock:
events = self._list_events()
if event:
events.append(event)
for event in events:
revoked_at = event.revoked_at
if revoked_at > oldest:
pruned.append(event)
self._store.set(_EVENT_KEY, pruned, lock)

View File

@ -0,0 +1,104 @@
# 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.
import uuid
from keystone.common import sql
from keystone import revoke
from keystone.revoke import model
class RevocationEvent(sql.ModelBase, sql.ModelDictMixin):
__tablename__ = 'revocation_event'
attributes = model.REVOKE_KEYS
# The id field is not going to be exposed to the outside world.
# It is, however, necessary for SQLAlchemy.
id = sql.Column(sql.String(64), primary_key=True)
domain_id = sql.Column(sql.String(64))
project_id = sql.Column(sql.String(64))
user_id = sql.Column(sql.String(64))
role_id = sql.Column(sql.String(64))
trust_id = sql.Column(sql.String(64))
consumer_id = sql.Column(sql.String(64))
access_token_id = sql.Column(sql.String(64))
issued_before = sql.Column(sql.DateTime(), nullable=False)
expires_at = sql.Column(sql.DateTime())
revoked_at = sql.Column(sql.DateTime(), nullable=False, index=True)
audit_id = sql.Column(sql.String(32))
audit_chain_id = sql.Column(sql.String(32))
class Revoke(revoke.RevokeDriverV8):
def _flush_batch_size(self, dialect):
batch_size = 0
if dialect == 'ibm_db_sa':
# This functionality is limited to DB2, because
# it is necessary to prevent the transaction log
# from filling up, whereas at least some of the
# other supported databases do not support update
# queries with LIMIT subqueries nor do they appear
# to require the use of such queries when deleting
# large numbers of records at once.
batch_size = 100
# Limit of 100 is known to not fill a transaction log
# of default maximum size while not significantly
# impacting the performance of large token purges on
# systems where the maximum transaction log size has
# been increased beyond the default.
return batch_size
def _prune_expired_events(self):
oldest = revoke.revoked_before_cutoff_time()
session = sql.get_session()
dialect = session.bind.dialect.name
batch_size = self._flush_batch_size(dialect)
if batch_size > 0:
query = session.query(RevocationEvent.id)
query = query.filter(RevocationEvent.revoked_at < oldest)
query = query.limit(batch_size).subquery()
delete_query = (session.query(RevocationEvent).
filter(RevocationEvent.id.in_(query)))
while True:
rowcount = delete_query.delete(synchronize_session=False)
if rowcount == 0:
break
else:
query = session.query(RevocationEvent)
query = query.filter(RevocationEvent.revoked_at < oldest)
query.delete(synchronize_session=False)
session.flush()
def list_events(self, last_fetch=None):
session = sql.get_session()
query = session.query(RevocationEvent).order_by(
RevocationEvent.revoked_at)
if last_fetch:
query = query.filter(RevocationEvent.revoked_at > last_fetch)
events = [model.RevokeEvent(**e.to_dict()) for e in query]
return events
def revoke(self, event):
kwargs = dict()
for attr in model.REVOKE_KEYS:
kwargs[attr] = getattr(event, attr)
kwargs['id'] = uuid.uuid4().hex
record = RevocationEvent(**kwargs)
session = sql.get_session()
with session.begin():
session.add(record)
self._prune_expired_events()

View File

@ -25,10 +25,10 @@ from keystone.common import cache
from keystone.common import dependency
from keystone.common import extension
from keystone.common import manager
from keystone.contrib.revoke import model
from keystone import exception
from keystone.i18n import _
from keystone import notifications
from keystone.revoke import model
CONF = cfg.CONF
@ -240,7 +240,7 @@ class RevokeDriverV8(object):
"""return the revocation events, as a list of objects
:param last_fetch: Time of last fetch. Return all events newer.
:returns: A list of keystone.contrib.revoke.model.RevokeEvent
:returns: A list of keystone.revoke.model.RevokeEvent
newer than `last_fetch.`
If no last_fetch is specified, returns all events
for tokens issued after the expiration cutoff.
@ -253,7 +253,7 @@ class RevokeDriverV8(object):
"""register a revocation event
:param event: An instance of
keystone.contrib.revoke.model.RevocationEvent
keystone.revoke.model.RevocationEvent
"""
raise exception.NotImplemented() # pragma: no cover

View File

@ -0,0 +1,29 @@
# 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 keystone.common import json_home
from keystone.common import wsgi
from keystone.revoke import controllers
class Routers(wsgi.RoutersBase):
PATH_PREFIX = '/OS-REVOKE'
def append_v3_routers(self, mapper, routers):
revoke_controller = controllers.RevokeController()
self._add_resource(
mapper, revoke_controller,
path=self.PATH_PREFIX + '/events',
get_action='list_revoke_events',
rel=json_home.build_v3_extension_resource_relation(
'OS-REVOKE', '1.0', 'events'))

View File

@ -15,7 +15,6 @@ from keystone import auth
from keystone import catalog
from keystone.common import cache
from keystone.contrib import endpoint_filter
from keystone.contrib import revoke
from keystone import credential
from keystone import endpoint_policy
from keystone import federation
@ -23,6 +22,7 @@ from keystone import identity
from keystone import oauth1
from keystone import policy
from keystone import resource
from keystone import revoke
from keystone import token
from keystone import trust

View File

@ -20,8 +20,8 @@ from six.moves import range
from testtools import matchers
from keystone.common import utils
from keystone.contrib.revoke import model
from keystone import exception
from keystone.revoke import model
from keystone.tests import unit
from keystone.tests.unit import test_backend_sql
from keystone.token import provider

View File

@ -21,6 +21,7 @@ import uuid
from keystoneclient.common import cms
import mock
from oslo_config import cfg
from oslo_log import versionutils
from oslo_utils import timeutils
from six.moves import http_client
from six.moves import range
@ -29,6 +30,7 @@ from testtools import testcase
from keystone import auth
from keystone.common import utils
from keystone.contrib.revoke import routers
from keystone import exception
from keystone.policy.backends import rules
from keystone.tests import unit
@ -1473,11 +1475,19 @@ class TestTokenRevokeByAssignment(TestTokenRevokeById):
self.assertIn(project_token, revoked_tokens)
class TestTokenRevokeApi(TestTokenRevokeById):
EXTENSION_NAME = 'revoke'
EXTENSION_TO_ADD = 'revoke_extension'
class RevokeContribTests(test_v3.RestfulTestCase):
@mock.patch.object(versionutils, 'report_deprecated_feature')
def test_exception_happens(self, mock_deprecator):
routers.RevokeExtension(mock.ANY)
mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY)
args, _kwargs = mock_deprecator.call_args
self.assertIn("Remove revoke_extension from", args[1])
class TestTokenRevokeApi(TestTokenRevokeById):
"""Test token revocation on the v3 Identity API."""
def config_overrides(self):
super(TestTokenRevokeApi, self).config_overrides()
self.config_fixture.config(group='revoke', driver='kvs')
@ -3099,8 +3109,6 @@ class TestTrustChain(test_v3.RestfulTestCase):
class TestTrustAuth(test_v3.RestfulTestCase):
EXTENSION_NAME = 'revoke'
EXTENSION_TO_ADD = 'revoke_extension'
def config_overrides(self):
super(TestTrustAuth, self).config_overrides()

View File

@ -19,7 +19,7 @@ from six.moves import http_client
from testtools import matchers
from keystone.common import utils
from keystone.contrib.revoke import model
from keystone.revoke import model
from keystone.tests.unit import test_v3
from keystone.token import provider
@ -31,8 +31,6 @@ def _future_time_string():
class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin):
EXTENSION_NAME = 'revoke'
EXTENSION_TO_ADD = 'revoke_extension'
JSON_HOME_DATA = {
'http://docs.openstack.org/api/openstack-identity/3/ext/OS-REVOKE/1.0/'

View File

@ -32,6 +32,7 @@ from keystone import identity
from keystone import oauth1
from keystone import policy
from keystone import resource
from keystone import revoke
from keystone import token
from keystone import trust
from keystone.version import controllers
@ -132,6 +133,7 @@ def v3_app_factory(global_conf, **local_conf):
identity,
policy,
resource,
revoke,
federation,
oauth1]

View File

@ -168,8 +168,8 @@ keystone.oauth1 =
sql = keystone.oauth1.backends.sql:OAuth1
keystone.revoke =
kvs = keystone.contrib.revoke.backends.kvs:Revoke
sql = keystone.contrib.revoke.backends.sql:Revoke
kvs = keystone.revoke.backends.kvs:Revoke
sql = keystone.revoke.backends.sql:Revoke
oslo.config.opts =
keystone = keystone.common.config:list_opts