From ea185a25a235d339d0d9282fbc08905fa1949b92 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Mon, 25 Aug 2014 20:17:47 -0700 Subject: [PATCH] Revoke by Audit Id / Audit Id Chain instead of expires Instead of using the expiry of the token which can collide (is non unique in some/many cases) use the new Audit ID for the tokens when revoking a single token via the token revocation events. Support for revoking by the audit_chain_id has been added to the token provider, however, the REST API has not been updated to accept an argument to revoke the chain. Support for revoking the entire chain is in place to allow Keystone to internally revoke an entire chain in certain circumstances. Exposing the ability to revoke the entire chain via the REST API may occur based upon further design discussions. Change-Id: I840355ccd9bcfcd88aa139184731c056808c2c8f bp: non-persistent-tokens Closes-Bug: 1292283 --- keystone/contrib/revoke/backends/sql.py | 2 + keystone/contrib/revoke/core.py | 12 ++ ..._add_audit_id_and_chain_to_revoke_table.py | 37 ++++++ keystone/contrib/revoke/model.py | 14 ++- keystone/tests/test_auth.py | 111 ++++++++++++++++++ keystone/tests/test_revoke.py | 72 +++++++++++- keystone/tests/test_sql_upgrade.py | 46 ++++++-- keystone/tests/test_v3_auth.py | 69 ++++++----- keystone/token/provider.py | 50 +++++--- 9 files changed, 348 insertions(+), 65 deletions(-) create mode 100644 keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py diff --git a/keystone/contrib/revoke/backends/sql.py b/keystone/contrib/revoke/backends/sql.py index 6c12974665..8d50ac6cc0 100644 --- a/keystone/contrib/revoke/backends/sql.py +++ b/keystone/contrib/revoke/backends/sql.py @@ -34,6 +34,8 @@ class RevocationEvent(sql.ModelBase, sql.ModelDictMixin): issued_before = sql.Column(sql.DateTime(), nullable=False) expires_at = sql.Column(sql.DateTime()) revoked_at = sql.Column(sql.DateTime(), nullable=False) + audit_id = sql.Column(sql.String(32)) + audit_chain_id = sql.Column(sql.String(32)) class Revoke(revoke.Driver): diff --git a/keystone/contrib/revoke/core.py b/keystone/contrib/revoke/core.py index 1df99bd471..9fc240790a 100644 --- a/keystone/contrib/revoke/core.py +++ b/keystone/contrib/revoke/core.py @@ -26,6 +26,7 @@ from keystone import exception from keystone.i18n import _ from keystone import notifications from keystone.openstack.common import log +from keystone.openstack.common import versionutils CONF = config.CONF @@ -128,6 +129,8 @@ class Manager(manager.Manager): def revoke_by_user(self, user_id): return self.revoke(model.RevokeEvent(user_id=user_id)) + @versionutils.deprecated(as_of=versionutils.deprecated.JUNO, + remove_in=0) def revoke_by_expiration(self, user_id, expires_at, domain_id=None, project_id=None): @@ -144,6 +147,15 @@ class Manager(manager.Manager): domain_id=domain_id, project_id=project_id)) + def revoke_by_audit_id(self, audit_id): + self.revoke(model.RevokeEvent(audit_id=audit_id)) + + def revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, + domain_id=None): + self.revoke(model.RevokeEvent(audit_chain_id=audit_chain_id, + domain_id=domain_id, + project_id=project_id)) + def revoke_by_grant(self, role_id, user_id=None, domain_id=None, project_id=None): self.revoke( diff --git a/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py b/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py new file mode 100644 index 0000000000..bee6fb2a5e --- /dev/null +++ b/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py @@ -0,0 +1,37 @@ +# 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 sqlalchemy as sql + + +_TABLE_NAME = 'revocation_event' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + event_table = sql.Table(_TABLE_NAME, meta, autoload=True) + audit_id_column = sql.Column('audit_id', sql.String(32), nullable=True) + audit_chain_column = sql.Column('audit_chain_id', sql.String(32), + nullable=True) + event_table.create_column(audit_id_column) + event_table.create_column(audit_chain_column) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + event_table = sql.Table(_TABLE_NAME, meta, autoload=True) + event_table.drop_column('audit_id') + event_table.drop_column('audit_chain_id') diff --git a/keystone/contrib/revoke/model.py b/keystone/contrib/revoke/model.py index e2cd0be0a7..dc913e6ee2 100644 --- a/keystone/contrib/revoke/model.py +++ b/keystone/contrib/revoke/model.py @@ -18,6 +18,8 @@ from oslo.utils import timeutils _NAMES = ['trust_id', 'consumer_id', 'access_token_id', + 'audit_id', + 'audit_chain_id', 'expires_at', 'domain_id', 'project_id', @@ -86,6 +88,8 @@ class RevokeEvent(object): 'domain_id', 'domain_scope_id', 'project_id', + 'audit_id', + 'audit_chain_id', ] event = dict((key, self.__dict__[key]) for key in keys if self.__dict__[key] is not None) @@ -257,7 +261,10 @@ def build_token_values_v2(access, default_domain_id): token_values = { 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['issued_at']))} + timeutils.parse_isotime(token_data['issued_at'])), + 'audit_id': token_data.get('audit_ids', [None])[0], + 'audit_chain_id': token_data.get('audit_ids', [None])[-1], + } token_values['user_id'] = access.get('user', {}).get('id') @@ -303,7 +310,10 @@ def build_token_values(token_data): token_values = { 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['issued_at']))} + timeutils.parse_isotime(token_data['issued_at'])), + 'audit_id': token_data.get('audit_ids', [None])[0], + 'audit_chain_id': token_data.get('audit_ids', [None])[-1], + } user = token_data.get('user') if user is not None: diff --git a/keystone/tests/test_auth.py b/keystone/tests/test_auth.py index de16c8e7a0..22547f638c 100644 --- a/keystone/tests/test_auth.py +++ b/keystone/tests/test_auth.py @@ -30,6 +30,7 @@ from keystone import tests from keystone.tests import default_fixtures from keystone.tests.ksfixtures import database from keystone import token +from keystone.token import provider from keystone import trust @@ -489,6 +490,116 @@ class AuthWithToken(AuthTest): self.assertThat(audit_ids, matchers.HasLength(2)) self.assertThat(audit_ids[-1], matchers.Equals(starting_audit_id)) + def test_revoke_by_audit_chain_id_original_token(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_id, revoke_chain=True) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + def test_revoke_by_audit_chain_id_chained_token(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_2_id, revoke_chain=True) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + def _mock_audit_info(self, parent_audit_id): + # NOTE(morgainfainberg): The token model and other cases that are + # extracting the audit id expect 'None' if the audit id doesn't + # exist. This ensures that the audit_id is None and the + # audit_chain_id will also return None. + return [None, None] + + def test_revoke_with_no_audit_info(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + with mock.patch.object(provider, 'audit_info', self._mock_audit_info): + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth( + token=unscoped_token['access']['token']) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_id, revoke_chain=True) + + revoke_events = self.revoke_api.get_events() + self.assertThat(revoke_events, matchers.HasLength(1)) + revoke_event = revoke_events[0].to_dict() + self.assertIn('expires_at', revoke_event) + self.assertEqual(unscoped_token_2['access']['token']['expires'], + revoke_event['expires_at']) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + # get a new token, with no audit info + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth( + token=unscoped_token['access']['token']) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + # Revoke by audit_id, no audit_info means both parent and child + # token are revoked. + self.token_provider_api.revoke_token(token_id) + + revoke_events = self.revoke_api.get_events() + self.assertThat(revoke_events, matchers.HasLength(2)) + revoke_event = revoke_events[1].to_dict() + self.assertIn('expires_at', revoke_event) + self.assertEqual(unscoped_token_2['access']['token']['expires'], + revoke_event['expires_at']) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + class AuthWithPasswordCredentials(AuthTest): def test_auth_invalid_user(self): diff --git a/keystone/tests/test_revoke.py b/keystone/tests/test_revoke.py index 07d0c1c969..d0a3089ffc 100644 --- a/keystone/tests/test_revoke.py +++ b/keystone/tests/test_revoke.py @@ -23,6 +23,7 @@ from keystone.contrib.revoke import model from keystone import exception from keystone import tests from keystone.tests import test_backend_sql +from keystone.token import provider def _new_id(): @@ -92,7 +93,7 @@ def _matches(event, token_values): # rest of the logic. attribute_names = ['project_id', 'expires_at', 'trust_id', 'consumer_id', - 'access_token_id'] + 'access_token_id', 'audit_id', 'audit_chain_id'] for attribute_name in attribute_names: if getattr(event, attribute_name) is not None: if (getattr(event, attribute_name) != @@ -267,6 +268,22 @@ class RevokeTreeTests(tests.TestCase): return self.tree.add_event( model.RevokeEvent(user_id=user_id)) + def _revoke_by_audit_id(self, audit_id): + event = self.tree.add_event( + model.RevokeEvent(audit_id=audit_id)) + self.events.append(event) + return event + + def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, + domain_id=None): + event = self.tree.add_event( + model.RevokeEvent(audit_chain_id=audit_chain_id, + project_id=project_id, + domain_id=domain_id) + ) + self.events.append(event) + return event + def _revoke_by_expiration(self, user_id, expires_at, project_id=None, domain_id=None): event = self.tree.add_event( @@ -355,6 +372,45 @@ class RevokeTreeTests(tests.TestCase): self.removeEvent(event) self._assertTokenNotRevoked(token_data_1) + def test_revoke_by_audit_id(self): + audit_id = provider.audit_info(parent_audit_id=None)[0] + token_data_1 = _sample_blank_token() + # Audit ID and Audit Chain ID are populated with the same value + # if the token is an original token + token_data_1['audit_id'] = audit_id + token_data_1['audit_chain_id'] = audit_id + event = self._revoke_by_audit_id(audit_id) + self._assertTokenRevoked(token_data_1) + + audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0] + token_data_2 = _sample_blank_token() + token_data_2['audit_id'] = audit_id_2 + token_data_2['audit_chain_id'] = audit_id + self._assertTokenNotRevoked(token_data_2) + + self.removeEvent(event) + self._assertTokenNotRevoked(token_data_1) + + def test_revoke_by_audit_chain_id(self): + audit_id = provider.audit_info(parent_audit_id=None)[0] + token_data_1 = _sample_blank_token() + # Audit ID and Audit Chain ID are populated with the same value + # if the token is an original token + token_data_1['audit_id'] = audit_id + token_data_1['audit_chain_id'] = audit_id + event = self._revoke_by_audit_chain_id(audit_id) + self._assertTokenRevoked(token_data_1) + + audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0] + token_data_2 = _sample_blank_token() + token_data_2['audit_id'] = audit_id_2 + token_data_2['audit_chain_id'] = audit_id + self._assertTokenRevoked(token_data_2) + + self.removeEvent(event) + self._assertTokenNotRevoked(token_data_1) + self._assertTokenNotRevoked(token_data_2) + def test_by_user_project(self): # When a user has a project-scoped token and the project-scoped token # is revoked then the token is revoked. @@ -515,18 +571,24 @@ class RevokeTreeTests(tests.TestCase): self.assertEqual(turn + 1, len(self.tree.revoke_map ['trust_id=*'] ['consumer_id=*'] - ['access_token_id=*'])) + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'])) # two different functions add domain_ids, +1 for None self.assertEqual(2 * turn + 1, len(self.tree.revoke_map ['trust_id=*'] ['consumer_id=*'] ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] ['expires_at=*'])) # two different functions add project_ids, +1 for None self.assertEqual(2 * turn + 1, len(self.tree.revoke_map ['trust_id=*'] ['consumer_id=*'] ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] ['expires_at=*'] ['domain_id=*'])) # 10 users added @@ -534,6 +596,8 @@ class RevokeTreeTests(tests.TestCase): ['trust_id=*'] ['consumer_id=*'] ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] ['expires_at=*'] ['domain_id=*'] ['project_id=*'])) @@ -554,7 +618,9 @@ class RevokeTreeTests(tests.TestCase): self.assertEqual(i + 2, len(self.tree.revoke_map ['trust_id=*'] ['consumer_id=*'] - ['access_token_id=*']), + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*']), 'adding %s to %s' % (args, self.tree.revoke_map)) diff --git a/keystone/tests/test_sql_upgrade.py b/keystone/tests/test_sql_upgrade.py index 077ca88d75..eef4b7a7fb 100644 --- a/keystone/tests/test_sql_upgrade.py +++ b/keystone/tests/test_sql_upgrade.py @@ -37,6 +37,7 @@ from migrate.versioning import api as versioning_api from oslo.db import exception as db_exception from oslo.db.sqlalchemy import migration from oslo.db.sqlalchemy import session as db_session +import six import sqlalchemy.exc from keystone.assignment.backends import sql as assignment_sql @@ -45,6 +46,7 @@ from keystone.common.sql import migrate_repo from keystone.common.sql import migration_helpers from keystone import config from keystone.contrib import federation +from keystone.contrib import revoke from keystone import exception from keystone import tests from keystone.tests import default_fixtures @@ -119,10 +121,14 @@ INITIAL_EXTENSION_TABLE_STRUCTURE = { 'revocation_event': [ 'id', 'domain_id', 'project_id', 'user_id', 'role_id', 'trust_id', 'consumer_id', 'access_token_id', - 'issued_before', 'expires_at', 'revoked_at', + 'issued_before', 'expires_at', 'revoked_at', 'audit_id', + 'audit_chain_id', ], } +EXTENSIONS = {'federation': federation, + 'revoke': revoke} + class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): def initialize_sql(self): @@ -1413,18 +1419,38 @@ class VersionTests(SqlMigrateBase): def test_extension_initial(self): """When get the initial version of an extension, it's 0.""" - abs_path = migration_helpers.find_migrate_repo(federation) - migration.db_version_control(sql.get_engine(), abs_path) - version = migration_helpers.get_db_version(extension='federation') - self.assertEqual(0, version) + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertEqual(0, version, + 'Migrate version for %s is not 0' % name) def test_extension_migrated(self): """When get the version after migrating an extension, it's not 0.""" - abs_path = migration_helpers.find_migrate_repo(federation) - migration.db_version_control(sql.get_engine(), abs_path) - migration.db_sync(sql.get_engine(), abs_path) - version = migration_helpers.get_db_version(extension='federation') - self.assertTrue(version > 0, "Version didn't change after migrated?") + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + migration.db_sync(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertTrue( + version > 0, + "Version for %s didn't change after migrated?" % name) + + def test_extension_downgraded(self): + """When get the version after downgrading an extension, it is 0.""" + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + migration.db_sync(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertTrue( + version > 0, + "Version for %s didn't change after migrated?" % name) + migration.db_sync(sql.get_engine(), abs_path, version=0) + version = migration_helpers.get_db_version(extension=name) + self.assertEqual(0, version, + 'Migrate version for %s is not 0' % name) def test_unexpected_extension(self): """The version for an extension that doesn't exist raises ImportError. diff --git a/keystone/tests/test_v3_auth.py b/keystone/tests/test_v3_auth.py index e1884cc0fd..0b2101ffa6 100644 --- a/keystone/tests/test_v3_auth.py +++ b/keystone/tests/test_v3_auth.py @@ -19,6 +19,7 @@ import uuid from keystoneclient.common import cms from oslo.utils import timeutils +import six from testtools import matchers from testtools import testcase @@ -1341,48 +1342,45 @@ class TestTokenRevokeApi(TestTokenRevokeById): expected_response = {'events': [{'domain_id': domain_id}]} self.assertEqual(expected_response, events_response) - def assertValidRevokedTokenResponse(self, events_response, user_id, - project_id=None): + def assertValidRevokedTokenResponse(self, events_response, **kwargs): events = events_response['events'] self.assertEqual(1, len(events)) - self.assertEqual(user_id, events[0]['user_id']) - if project_id: - self.assertEqual(project_id, events[0]['project_id']) - self.assertIsNotNone(events[0]['expires_at']) + for k, v in six.iteritems(kwargs): + self.assertEqual(v, events[0].get(k)) self.assertIsNotNone(events[0]['issued_before']) self.assertIsNotNone(events_response['links']) - del (events_response['events'][0]['expires_at']) del (events_response['events'][0]['issued_before']) del (events_response['links']) - expected_event_data = {'user_id': user_id} - if project_id: - expected_event_data['project_id'] = project_id - expected_response = {'events': [expected_event_data]} + expected_response = {'events': [kwargs]} self.assertEqual(expected_response, events_response) def test_revoke_token(self): scoped_token = self.get_scoped_token() headers = {'X-Subject-Token': scoped_token} - self.head('/auth/tokens', headers=headers, expected_status=200) + response = self.get('/auth/tokens', headers=headers, + expected_status=200).json_body['token'] + self.delete('/auth/tokens', headers=headers, expected_status=204) self.head('/auth/tokens', headers=headers, expected_status=404) events_response = self.get('/OS-REVOKE/events', expected_status=200).json_body - self.assertValidRevokedTokenResponse(events_response, self.user['id'], - project_id=self.project['id']) + self.assertValidRevokedTokenResponse(events_response, + audit_id=response['audit_ids'][0]) def test_revoke_v2_token(self): token = self.get_v2_token() headers = {'X-Subject-Token': token} - self.head('/auth/tokens', headers=headers, expected_status=200) + response = self.get('/auth/tokens', headers=headers, + expected_status=200).json_body['token'] self.delete('/auth/tokens', headers=headers, expected_status=204) self.head('/auth/tokens', headers=headers, expected_status=404) events_response = self.get('/OS-REVOKE/events', expected_status=200).json_body - self.assertValidRevokedTokenResponse(events_response, - self.default_domain_user['id']) + self.assertValidRevokedTokenResponse( + events_response, + audit_id=response['audit_ids'][0]) def test_revoke_by_id_false_410(self): self.get('/auth/tokens/OS-PKI/revoked', expected_status=410) @@ -1414,19 +1412,30 @@ class TestTokenRevokeApi(TestTokenRevokeById): self.assertDomainInList(events, self.domainA['id']) - def assertUserAndExpiryInList(self, events, user_id, expires_at): + def assertEventDataInList(self, events, **kwargs): found = False for e in events: - - # Timestamps in the event list are accurate to second. - expires_at = timeutils.parse_isotime(expires_at) - expires_at = timeutils.isotime(expires_at) - - if e['user_id'] == user_id and e['expires_at'] == expires_at: + for key, value in six.iteritems(kwargs): + try: + if e[key] != value: + break + except KeyError: + # Break the loop and present a nice error instead of + # KeyError + break + else: + # If the value of the event[key] matches the value of the kwarg + # for each item in kwargs, the event was fully matched and + # the assertTrue below should succeed. found = True self.assertTrue(found, - 'event with correct user_id %s and expires_at value ' - 'not in list' % user_id) + 'event with correct values not in list, expected to ' + 'find event with key-value pairs. Expected: ' + '"%(expected)s" Events: "%(events)s"' % + {'expected': ','.join( + ["'%s=%s'" % (k, v) for k, v in six.iteritems( + kwargs)]), + 'events': events}) def test_list_delete_token_shows_in_event_list(self): self.role_data_fixtures() @@ -1456,11 +1465,9 @@ class TestTokenRevokeApi(TestTokenRevokeById): expected_status=200).json_body events = events_response['events'] self.assertEqual(1, len(events)) - self.assertUserAndExpiryInList(events, - token2['user']['id'], - token2['expires_at']) - self.assertValidRevokedTokenResponse(events_response, self.user['id'], - project_id=self.project['id']) + self.assertEventDataInList( + events, + audit_id=token2['audit_ids'][1]) self.head('/auth/tokens', headers=headers, expected_status=404) self.head('/auth/tokens', headers=headers2, expected_status=200) self.head('/auth/tokens', headers=headers3, expected_status=200) diff --git a/keystone/token/provider.py b/keystone/token/provider.py index 6b1e38a308..2c6ff80f87 100644 --- a/keystone/token/provider.py +++ b/keystone/token/provider.py @@ -411,31 +411,43 @@ class Manager(manager.Manager): self._validate_v2_token.invalidate(self, token_id) self._validate_v3_token.invalidate(self, token_id) - def revoke_token(self, token_id): + def revoke_token(self, token_id, revoke_chain=False): if self.revoke_api: - user_id = None - expires_at = None - domain_id = None + revoke_by_expires = False project_id = None + domain_id = None - token_ref = self.persistence.get_token(token_id) - version = self.driver.get_token_version(token_ref) + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.validate_token(token_id)) - if version == self.V3: - user_id = token_ref['user']['id'] - expires_at = token_ref['expires'] + user_id = token_ref.user_id + expires_at = token_ref.expires + audit_id = token_ref.audit_id + audit_chain_id = token_ref.audit_chain_id + if token_ref.project_scoped: + project_id = token_ref.project_id + if token_ref.domain_scoped: + domain_id = token_ref.domain_id - token_data = token_ref['token_data']['token'] - project_id = token_data.get('project', {}).get('id') - domain_id = token_data.get('domain', {}).get('id') - elif version == self.V2: - user_id = token_ref['user_id'] - expires_at = token_ref['expires'] - project_id = (token_ref.get('tenant') or {}).get('id') + if audit_id is None and not revoke_chain: + LOG.debug('Received token with no audit_id.') + revoke_by_expires = True - self.revoke_api.revoke_by_expiration(user_id, expires_at, - project_id=project_id, - domain_id=domain_id) + if audit_chain_id is None and revoke_chain: + LOG.debug('Received token with no audit_chain_id.', token_id) + revoke_by_expires = True + + if revoke_by_expires: + self.revoke_api.revoke_by_expiration(user_id, expires_at, + project_id=project_id, + domain_id=domain_id) + elif revoke_chain: + self.revoke_api.revoke_by_audit_chain_id(audit_chain_id, + project_id=project_id, + domain_id=domain_id) + else: + self.revoke_api.revoke_by_audit_id(audit_id) if CONF.token.revoke_by_id: self.persistence.delete_token(token_id=token_id)