diff --git a/etc/keystone-paste.ini b/etc/keystone-paste.ini index 11229aa714..7adedcb57c 100644 --- a/etc/keystone-paste.ini +++ b/etc/keystone-paste.ini @@ -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 diff --git a/keystone/contrib/revoke/__init__.py b/keystone/contrib/revoke/__init__.py index 58ba68db37..e69de29bb2 100644 --- a/keystone/contrib/revoke/__init__.py +++ b/keystone/contrib/revoke/__init__.py @@ -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 diff --git a/keystone/contrib/revoke/backends/kvs.py b/keystone/contrib/revoke/backends/kvs.py index 086becb07b..104b64e93f 100644 --- a/keystone/contrib/revoke/backends/kvs.py +++ b/keystone/contrib/revoke/backends/kvs.py @@ -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) diff --git a/keystone/contrib/revoke/backends/sql.py b/keystone/contrib/revoke/backends/sql.py index 82e05194f7..0bf493ae76 100644 --- a/keystone/contrib/revoke/backends/sql.py +++ b/keystone/contrib/revoke/backends/sql.py @@ -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) diff --git a/keystone/contrib/revoke/routers.py b/keystone/contrib/revoke/routers.py index 4d2edfc090..a44c619435 100644 --- a/keystone/contrib/revoke/routers.py +++ b/keystone/contrib/revoke/routers.py @@ -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) diff --git a/keystone/revoke/__init__.py b/keystone/revoke/__init__.py new file mode 100644 index 0000000000..22bcf0cf37 --- /dev/null +++ b/keystone/revoke/__init__.py @@ -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 diff --git a/keystone/revoke/backends/__init__.py b/keystone/revoke/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/revoke/backends/kvs.py b/keystone/revoke/backends/kvs.py new file mode 100644 index 0000000000..c9b7d26c6e --- /dev/null +++ b/keystone/revoke/backends/kvs.py @@ -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) diff --git a/keystone/revoke/backends/sql.py b/keystone/revoke/backends/sql.py new file mode 100644 index 0000000000..5bf7d84ec3 --- /dev/null +++ b/keystone/revoke/backends/sql.py @@ -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() diff --git a/keystone/contrib/revoke/controllers.py b/keystone/revoke/controllers.py similarity index 100% rename from keystone/contrib/revoke/controllers.py rename to keystone/revoke/controllers.py diff --git a/keystone/contrib/revoke/core.py b/keystone/revoke/core.py similarity index 98% rename from keystone/contrib/revoke/core.py rename to keystone/revoke/core.py index fe91b721d3..a32b2bfcc3 100644 --- a/keystone/contrib/revoke/core.py +++ b/keystone/revoke/core.py @@ -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 diff --git a/keystone/contrib/revoke/model.py b/keystone/revoke/model.py similarity index 100% rename from keystone/contrib/revoke/model.py rename to keystone/revoke/model.py diff --git a/keystone/revoke/routers.py b/keystone/revoke/routers.py new file mode 100644 index 0000000000..aab78493bc --- /dev/null +++ b/keystone/revoke/routers.py @@ -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')) diff --git a/keystone/server/backends.py b/keystone/server/backends.py index c71f747826..d7c8e9291f 100644 --- a/keystone/server/backends.py +++ b/keystone/server/backends.py @@ -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 diff --git a/keystone/tests/unit/test_revoke.py b/keystone/tests/unit/test_revoke.py index 0c364b8dbd..6f75377540 100644 --- a/keystone/tests/unit/test_revoke.py +++ b/keystone/tests/unit/test_revoke.py @@ -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 diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py index 3d8e353a46..e22511e01c 100644 --- a/keystone/tests/unit/test_v3_auth.py +++ b/keystone/tests/unit/test_v3_auth.py @@ -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() diff --git a/keystone/tests/unit/test_v3_os_revoke.py b/keystone/tests/unit/test_v3_os_revoke.py index 86ced7248d..1ad614d937 100644 --- a/keystone/tests/unit/test_v3_os_revoke.py +++ b/keystone/tests/unit/test_v3_os_revoke.py @@ -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/' diff --git a/keystone/version/service.py b/keystone/version/service.py index 789fab7f6f..6a6ed6c4c2 100644 --- a/keystone/version/service.py +++ b/keystone/version/service.py @@ -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] diff --git a/setup.cfg b/setup.cfg index eada13d53e..ba85c93561 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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