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
changes/04/235704/14
Steve Martinelli 7 years ago committed by Dave Chen
parent a83b6b29e8
commit 32b70d15c4
  1. 5
      etc/keystone-paste.ini
  2. 13
      keystone/contrib/revoke/__init__.py
  3. 65
      keystone/contrib/revoke/backends/kvs.py
  4. 96
      keystone/contrib/revoke/backends/sql.py
  5. 26
      keystone/contrib/revoke/routers.py
  6. 14
      keystone/revoke/__init__.py
  7. 0
      keystone/revoke/backends/__init__.py
  8. 74
      keystone/revoke/backends/kvs.py
  9. 104
      keystone/revoke/backends/sql.py
  10. 0
      keystone/revoke/controllers.py
  11. 6
      keystone/revoke/core.py
  12. 0
      keystone/revoke/model.py
  13. 29
      keystone/revoke/routers.py
  14. 2
      keystone/server/backends.py
  15. 2
      keystone/tests/unit/test_revoke.py
  16. 18
      keystone/tests/unit/test_v3_auth.py
  17. 4
      keystone/tests/unit/test_v3_os_revoke.py
  18. 2
      keystone/version/service.py
  19. 4
      setup.cfg

@ -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

@ -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

@ -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
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.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 = []
from keystone.revoke.backends import kvs
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
_OLD = 'keystone.contrib.revoke.backends.kvs.Revoke'
_NEW = 'kvs'
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)
class Revoke(kvs.Revoke):
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)

@ -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
_OLD = "keystone.contrib.revoke.backends.sql.Revoke"
_NEW = "sql"
# 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(sql.Revoke):
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()
@versionutils.deprecated(versionutils.deprecated.MITAKA,
in_favor_of=_NEW,
what=_OLD)
def __init__(self, *args, **kwargs):
super(Revoke, self).__init__(*args, **kwargs)

@ -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 _
LOG = log.getLogger(__name__)
class RevokeExtension(wsgi.V3ExtensionRouter):
PATH_PREFIX = '/OS-REVOKE'
class RevokeExtension(wsgi.Middleware):
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'))
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)

@ -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

@ -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)

@ -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()

@ -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

@ -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'))

@ -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

@ -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

@ -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()

@ -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/'

@ -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]

@ -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

Loading…
Cancel
Save