summaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/contrib/revoke
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/contrib/revoke')
-rw-r--r--keystone-moon/keystone/contrib/revoke/__init__.py13
-rw-r--r--keystone-moon/keystone/contrib/revoke/backends/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/revoke/backends/kvs.py73
-rw-r--r--keystone-moon/keystone/contrib/revoke/backends/sql.py104
-rw-r--r--keystone-moon/keystone/contrib/revoke/controllers.py44
-rw-r--r--keystone-moon/keystone/contrib/revoke/core.py250
-rw-r--r--keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg25
-rw-r--r--keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py47
-rw-r--r--keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py37
-rw-r--r--keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/revoke/model.py365
-rw-r--r--keystone-moon/keystone/contrib/revoke/routers.py29
13 files changed, 987 insertions, 0 deletions
diff --git a/keystone-moon/keystone/contrib/revoke/__init__.py b/keystone-moon/keystone/contrib/revoke/__init__.py
new file mode 100644
index 00000000..58ba68db
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/__init__.py
@@ -0,0 +1,13 @@
+# 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-moon/keystone/contrib/revoke/backends/__init__.py b/keystone-moon/keystone/contrib/revoke/backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/backends/__init__.py
diff --git a/keystone-moon/keystone/contrib/revoke/backends/kvs.py b/keystone-moon/keystone/contrib/revoke/backends/kvs.py
new file mode 100644
index 00000000..cc41fbee
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/backends/kvs.py
@@ -0,0 +1,73 @@
+# 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_utils import timeutils
+
+from keystone.common import kvs
+from keystone.contrib import revoke
+from keystone import exception
+from keystone.openstack.common import versionutils
+
+
+CONF = cfg.CONF
+
+_EVENT_KEY = 'os-revoke-events'
+_KVS_BACKEND = 'openstack.kvs.Memory'
+
+
+class Revoke(revoke.Driver):
+
+ @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 _prune_expired_events_and_get(self, last_fetch=None, new_event=None):
+ pruned = []
+ results = []
+ expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
+ oldest = timeutils.utcnow() - expire_delta
+ # TODO(ayoung): Store the time of the oldest event so that the
+ # prune process can be skipped if none of the events have timed out.
+ with self._store.get_lock(_EVENT_KEY) as lock:
+ events = self._list_events()
+ if new_event is not None:
+ events.append(new_event)
+
+ for event in events:
+ revoked_at = event.revoked_at
+ if revoked_at > oldest:
+ pruned.append(event)
+ if last_fetch is None or revoked_at > last_fetch:
+ results.append(event)
+ self._store.set(_EVENT_KEY, pruned, lock)
+ return results
+
+ def list_events(self, last_fetch=None):
+ return self._prune_expired_events_and_get(last_fetch=last_fetch)
+
+ def revoke(self, event):
+ self._prune_expired_events_and_get(new_event=event)
diff --git a/keystone-moon/keystone/contrib/revoke/backends/sql.py b/keystone-moon/keystone/contrib/revoke/backends/sql.py
new file mode 100644
index 00000000..1b0cde1e
--- /dev/null
+++ b/keystone-moon/keystone/contrib/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.contrib import revoke
+from keystone.contrib.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)
+ audit_id = sql.Column(sql.String(32))
+ audit_chain_id = sql.Column(sql.String(32))
+
+
+class Revoke(revoke.Driver):
+ 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):
+ self._prune_expired_events()
+ 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)
diff --git a/keystone-moon/keystone/contrib/revoke/controllers.py b/keystone-moon/keystone/contrib/revoke/controllers.py
new file mode 100644
index 00000000..40151bae
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/controllers.py
@@ -0,0 +1,44 @@
+# 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 oslo_utils import timeutils
+
+from keystone.common import controller
+from keystone.common import dependency
+from keystone import exception
+from keystone.i18n import _
+
+
+@dependency.requires('revoke_api')
+class RevokeController(controller.V3Controller):
+ @controller.protected()
+ def list_revoke_events(self, context):
+ since = context['query_string'].get('since')
+ last_fetch = None
+ if since:
+ try:
+ last_fetch = timeutils.normalize_time(
+ timeutils.parse_isotime(since))
+ except ValueError:
+ raise exception.ValidationError(
+ message=_('invalid date format %s') % since)
+ events = self.revoke_api.list_events(last_fetch=last_fetch)
+ # Build the links by hand as the standard controller calls require ids
+ response = {'events': [event.to_dict() for event in events],
+ 'links': {
+ 'next': None,
+ 'self': RevokeController.base_url(
+ context,
+ path=context['path']),
+ 'previous': None}
+ }
+ return response
diff --git a/keystone-moon/keystone/contrib/revoke/core.py b/keystone-moon/keystone/contrib/revoke/core.py
new file mode 100644
index 00000000..c7335690
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/core.py
@@ -0,0 +1,250 @@
+# 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 abc
+import datetime
+
+from oslo_config import cfg
+from oslo_log import log
+from oslo_utils import timeutils
+import six
+
+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.openstack.common import versionutils
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+EXTENSION_DATA = {
+ 'name': 'OpenStack Revoke API',
+ 'namespace': 'http://docs.openstack.org/identity/api/ext/'
+ 'OS-REVOKE/v1.0',
+ 'alias': 'OS-REVOKE',
+ 'updated': '2014-02-24T20:51:0-00:00',
+ 'description': 'OpenStack revoked token reporting mechanism.',
+ 'links': [
+ {
+ 'rel': 'describedby',
+ 'type': 'text/html',
+ 'href': ('https://github.com/openstack/identity-api/blob/master/'
+ 'openstack-identity-api/v3/src/markdown/'
+ 'identity-api-v3-os-revoke-ext.md'),
+ }
+ ]}
+extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
+extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
+
+MEMOIZE = cache.get_memoization_decorator(section='revoke')
+
+
+def revoked_before_cutoff_time():
+ expire_delta = datetime.timedelta(
+ seconds=CONF.token.expiration + CONF.revoke.expiration_buffer)
+ oldest = timeutils.utcnow() - expire_delta
+ return oldest
+
+
+@dependency.provider('revoke_api')
+class Manager(manager.Manager):
+ """Revoke API Manager.
+
+ Performs common logic for recording revocations.
+
+ """
+
+ def __init__(self):
+ super(Manager, self).__init__(CONF.revoke.driver)
+ self._register_listeners()
+ self.model = model
+
+ def _user_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke_by_user(payload['resource_info'])
+
+ def _role_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(role_id=payload['resource_info']))
+
+ def _project_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(project_id=payload['resource_info']))
+
+ def _domain_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(domain_id=payload['resource_info']))
+
+ def _trust_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(trust_id=payload['resource_info']))
+
+ def _consumer_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(consumer_id=payload['resource_info']))
+
+ def _access_token_callback(self, service, resource_type, operation,
+ payload):
+ self.revoke(
+ model.RevokeEvent(access_token_id=payload['resource_info']))
+
+ def _group_callback(self, service, resource_type, operation, payload):
+ user_ids = (u['id'] for u in self.identity_api.list_users_in_group(
+ payload['resource_info']))
+ for uid in user_ids:
+ self.revoke(model.RevokeEvent(user_id=uid))
+
+ def _register_listeners(self):
+ callbacks = {
+ notifications.ACTIONS.deleted: [
+ ['OS-TRUST:trust', self._trust_callback],
+ ['OS-OAUTH1:consumer', self._consumer_callback],
+ ['OS-OAUTH1:access_token', self._access_token_callback],
+ ['role', self._role_callback],
+ ['user', self._user_callback],
+ ['project', self._project_callback],
+ ],
+ notifications.ACTIONS.disabled: [
+ ['user', self._user_callback],
+ ['project', self._project_callback],
+ ['domain', self._domain_callback],
+ ],
+ notifications.ACTIONS.internal: [
+ [notifications.INVALIDATE_USER_TOKEN_PERSISTENCE,
+ self._user_callback],
+ ]
+ }
+
+ for event, cb_info in six.iteritems(callbacks):
+ for resource_type, callback_fns in cb_info:
+ notifications.register_event_callback(event, resource_type,
+ callback_fns)
+
+ def revoke_by_user(self, user_id):
+ return self.revoke(model.RevokeEvent(user_id=user_id))
+
+ def _assert_not_domain_and_project_scoped(self, domain_id=None,
+ project_id=None):
+ if domain_id is not None and project_id is not None:
+ msg = _('The revoke call must not have both domain_id and '
+ 'project_id. This is a bug in the Keystone server. The '
+ 'current request is aborted.')
+ raise exception.UnexpectedError(exception=msg)
+
+ @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):
+
+ self._assert_not_domain_and_project_scoped(domain_id=domain_id,
+ project_id=project_id)
+
+ self.revoke(
+ model.RevokeEvent(user_id=user_id,
+ expires_at=expires_at,
+ 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._assert_not_domain_and_project_scoped(domain_id=domain_id,
+ project_id=project_id)
+
+ 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(
+ 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.revoke(
+ model.RevokeEvent(project_id=project_id, user_id=user_id))
+
+ def revoke_by_project_role_assignment(self, project_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.revoke(model.RevokeEvent(domain_id=domain_id, role_id=role_id))
+
+ @MEMOIZE
+ def _get_revoke_tree(self):
+ events = self.driver.list_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
+
+ :param token_values: dictionary of values from a token,
+ normalized for differences between v2 and v3. The checked values are a
+ subset of the attributes of model.TokenEvent
+
+ :raises exception.TokenNotFound: if the token is invalid
+
+ """
+ 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):
+ """Interface for recording and reporting revocation events."""
+
+ @abc.abstractmethod
+ def list_events(self, last_fetch=None):
+ """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
+ newer than `last_fetch.`
+ If no last_fetch is specified, returns all events
+ for tokens issued after the expiration cutoff.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def revoke(self, event):
+ """register a revocation event
+
+ :param event: An instance of
+ keystone.contrib.revoke.model.RevocationEvent
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py
diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg
new file mode 100644
index 00000000..0e61bcaa
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg
@@ -0,0 +1,25 @@
+[db_settings]
+# Used to identify which repository this database is versioned under.
+# You can use the name of your project.
+repository_id=revoke
+
+# The name of the database table used to track the schema version.
+# This name shouldn't already be used by your project.
+# If this is changed once a database is under version control, you'll need to
+# change the table name in each database too.
+version_table=migrate_version
+
+# When committing a change script, Migrate will attempt to generate the
+# sql for all supported databases; normally, if one of them fails - probably
+# because you don't have that database installed - it is ignored and the
+# commit continues, perhaps ending successfully.
+# Databases in this list MUST compile successfully during a commit, or the
+# entire commit will fail. List the databases your application will actually
+# be using to ensure your updates to that database work properly.
+# This must be a list; example: ['postgres','sqlite']
+required_dbs=[]
+
+# When creating new change scripts, Migrate will stamp the new script with
+# a version number. By default this is latest_version + 1. You can set this
+# to 'true' to tell Migrate to use the UTC timestamp instead.
+use_timestamp_numbering=False
diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py
new file mode 100644
index 00000000..7927ce0c
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py
@@ -0,0 +1,47 @@
+# 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
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine; bind
+ # migrate_engine to your metadata
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ service_table = sql.Table(
+ 'revocation_event',
+ meta,
+ sql.Column('id', sql.String(64), primary_key=True),
+ sql.Column('domain_id', sql.String(64)),
+ sql.Column('project_id', sql.String(64)),
+ sql.Column('user_id', sql.String(64)),
+ sql.Column('role_id', sql.String(64)),
+ sql.Column('trust_id', sql.String(64)),
+ sql.Column('consumer_id', sql.String(64)),
+ sql.Column('access_token_id', sql.String(64)),
+ sql.Column('issued_before', sql.DateTime(), nullable=False),
+ sql.Column('expires_at', sql.DateTime()),
+ sql.Column('revoked_at', sql.DateTime(), index=True, nullable=False))
+ service_table.create(migrate_engine, checkfirst=True)
+
+
+def downgrade(migrate_engine):
+ # Operations to reverse the above upgrade go here.
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ tables = ['revocation_event']
+ for t in tables:
+ table = sql.Table(t, meta, autoload=True)
+ table.drop(migrate_engine, checkfirst=True)
diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py
new file mode 100644
index 00000000..bee6fb2a
--- /dev/null
+++ b/keystone-moon/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-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py
diff --git a/keystone-moon/keystone/contrib/revoke/model.py b/keystone-moon/keystone/contrib/revoke/model.py
new file mode 100644
index 00000000..5e92042d
--- /dev/null
+++ b/keystone-moon/keystone/contrib/revoke/model.py
@@ -0,0 +1,365 @@
+# 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 oslo_utils import timeutils
+
+
+# The set of attributes common between the RevokeEvent
+# and the dictionaries created from the token Data.
+_NAMES = ['trust_id',
+ 'consumer_id',
+ 'access_token_id',
+ 'audit_id',
+ 'audit_chain_id',
+ 'expires_at',
+ 'domain_id',
+ 'project_id',
+ 'user_id',
+ 'role_id']
+
+
+# Additional arguments for creating a RevokeEvent
+_EVENT_ARGS = ['issued_before', 'revoked_at']
+
+# Names of attributes in the RevocationEvent, including "virtual" attributes.
+# Virtual attributes are those added based on other values.
+_EVENT_NAMES = _NAMES + ['domain_scope_id']
+
+# Values that will be in the token data but not in the event.
+# These will compared with event values that have different names.
+# For example: both trustor_id and trustee_id are compared against user_id
+_TOKEN_KEYS = ['identity_domain_id',
+ 'assignment_domain_id',
+ 'issued_at',
+ 'trustor_id',
+ 'trustee_id']
+
+
+REVOKE_KEYS = _NAMES + _EVENT_ARGS
+
+
+def blank_token_data(issued_at):
+ token_data = dict()
+ for name in _NAMES:
+ token_data[name] = None
+ for name in _TOKEN_KEYS:
+ token_data[name] = None
+ # required field
+ token_data['issued_at'] = issued_at
+ return token_data
+
+
+class RevokeEvent(object):
+ def __init__(self, **kwargs):
+ for k in REVOKE_KEYS:
+ v = kwargs.get(k, None)
+ setattr(self, k, v)
+
+ if self.domain_id and self.expires_at:
+ # This is revoking a domain-scoped token.
+ self.domain_scope_id = self.domain_id
+ self.domain_id = None
+ else:
+ # This is revoking all tokens for a domain.
+ self.domain_scope_id = None
+
+ if self.expires_at is not None:
+ # Trim off the expiration time because MySQL timestamps are only
+ # accurate to the second.
+ self.expires_at = self.expires_at.replace(microsecond=0)
+
+ if self.revoked_at is None:
+ self.revoked_at = timeutils.utcnow()
+ if self.issued_before is None:
+ self.issued_before = self.revoked_at
+
+ def to_dict(self):
+ keys = ['user_id',
+ 'role_id',
+ 'domain_id',
+ 'domain_scope_id',
+ 'project_id',
+ 'audit_id',
+ 'audit_chain_id',
+ ]
+ event = {key: self.__dict__[key] for key in keys
+ if self.__dict__[key] is not None}
+ if self.trust_id is not None:
+ event['OS-TRUST:trust_id'] = self.trust_id
+ if self.consumer_id is not None:
+ event['OS-OAUTH1:consumer_id'] = self.consumer_id
+ if self.consumer_id is not None:
+ event['OS-OAUTH1:access_token_id'] = self.access_token_id
+ if self.expires_at is not None:
+ event['expires_at'] = timeutils.isotime(self.expires_at)
+ if self.issued_before is not None:
+ event['issued_before'] = timeutils.isotime(self.issued_before,
+ subsecond=True)
+ return event
+
+ def key_for_name(self, name):
+ return "%s=%s" % (name, getattr(self, name) or '*')
+
+
+def attr_keys(event):
+ return map(event.key_for_name, _EVENT_NAMES)
+
+
+class RevokeTree(object):
+ """Fast Revocation Checking Tree Structure
+
+ The Tree is an index to quickly match tokens against events.
+ Each node is a hashtable of key=value combinations from revocation events.
+ The
+
+ """
+
+ def __init__(self, revoke_events=None):
+ self.revoke_map = dict()
+ self.add_events(revoke_events)
+
+ def add_event(self, event):
+ """Updates the tree based on a revocation event.
+
+ Creates any necessary internal nodes in the tree corresponding to the
+ fields of the revocation event. The leaf node will always be set to
+ the latest 'issued_before' for events that are otherwise identical.
+
+ :param: Event to add to the tree
+
+ :returns: the event that was passed in.
+
+ """
+ revoke_map = self.revoke_map
+ for key in attr_keys(event):
+ revoke_map = revoke_map.setdefault(key, {})
+ revoke_map['issued_before'] = max(
+ event.issued_before, revoke_map.get(
+ 'issued_before', event.issued_before))
+ return event
+
+ def remove_event(self, event):
+ """Update the tree based on the removal of a Revocation Event
+
+ Removes empty nodes from the tree from the leaf back to the root.
+
+ If multiple events trace the same path, but have different
+ 'issued_before' values, only the last is ever stored in the tree.
+ So only an exact match on 'issued_before' ever triggers a removal
+
+ :param: Event to remove from the tree
+
+ """
+ stack = []
+ revoke_map = self.revoke_map
+ for name in _EVENT_NAMES:
+ key = event.key_for_name(name)
+ nxt = revoke_map.get(key)
+ if nxt is None:
+ break
+ stack.append((revoke_map, key, nxt))
+ revoke_map = nxt
+ else:
+ if event.issued_before == revoke_map['issued_before']:
+ revoke_map.pop('issued_before')
+ for parent, key, child in reversed(stack):
+ if not any(child):
+ del parent[key]
+
+ def add_events(self, revoke_events):
+ return map(self.add_event, revoke_events or [])
+
+ def is_revoked(self, token_data):
+ """Check if a token matches the revocation event
+
+ Compare the values for each level of the tree with the values from
+ the token, accounting for attributes that have alternative
+ keys, and for wildcard matches.
+ if there is a match, continue down the tree.
+ if there is no match, exit early.
+
+ token_data is a map based on a flattened view of token.
+ The required fields are:
+
+ 'expires_at','user_id', 'project_id', 'identity_domain_id',
+ 'assignment_domain_id', 'trust_id', 'trustor_id', 'trustee_id'
+ 'consumer_id', 'access_token_id'
+
+ """
+ # Alternative names to be checked in token for every field in
+ # revoke tree.
+ alternatives = {
+ 'user_id': ['user_id', 'trustor_id', 'trustee_id'],
+ 'domain_id': ['identity_domain_id', 'assignment_domain_id'],
+ # For a domain-scoped token, the domain is in assignment_domain_id.
+ 'domain_scope_id': ['assignment_domain_id', ],
+ }
+ # Contains current forest (collection of trees) to be checked.
+ partial_matches = [self.revoke_map]
+ # We iterate over every layer of our revoke tree (except the last one).
+ for name in _EVENT_NAMES:
+ # bundle is the set of partial matches for the next level down
+ # the tree
+ bundle = []
+ wildcard = '%s=*' % (name,)
+ # For every tree in current forest.
+ for tree in partial_matches:
+ # If there is wildcard node on current level we take it.
+ bundle.append(tree.get(wildcard))
+ if name == 'role_id':
+ # Roles are very special since a token has a list of them.
+ # If the revocation event matches any one of them,
+ # revoke the token.
+ for role_id in token_data.get('roles', []):
+ bundle.append(tree.get('role_id=%s' % role_id))
+ else:
+ # For other fields we try to get any branch that concur
+ # with any alternative field in the token.
+ for alt_name in alternatives.get(name, [name]):
+ bundle.append(
+ tree.get('%s=%s' % (name, token_data[alt_name])))
+ # tree.get returns `None` if there is no match, so `bundle.append`
+ # adds a 'None' entry. This call remoes the `None` entries.
+ partial_matches = [x for x in bundle if x is not None]
+ if not partial_matches:
+ # If we end up with no branches to follow means that the token
+ # is definitely not in the revoke tree and all further
+ # iterations will be for nothing.
+ return False
+
+ # The last (leaf) level is checked in a special way because we verify
+ # issued_at field differently.
+ for leaf in partial_matches:
+ try:
+ if leaf['issued_before'] > token_data['issued_at']:
+ return True
+ except KeyError:
+ pass
+ # If we made it out of the loop then no element in revocation tree
+ # corresponds to our token and it is good.
+ return False
+
+
+def build_token_values_v2(access, default_domain_id):
+ token_data = access['token']
+
+ token_expires_at = timeutils.parse_isotime(token_data['expires'])
+
+ # Trim off the microseconds because the revocation event only has
+ # expirations accurate to the second.
+ token_expires_at = token_expires_at.replace(microsecond=0)
+
+ token_values = {
+ 'expires_at': timeutils.normalize_time(token_expires_at),
+ 'issued_at': timeutils.normalize_time(
+ 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')
+
+ project = token_data.get('tenant')
+ if project is not None:
+ token_values['project_id'] = project['id']
+ else:
+ token_values['project_id'] = None
+
+ token_values['identity_domain_id'] = default_domain_id
+ token_values['assignment_domain_id'] = default_domain_id
+
+ trust = token_data.get('trust')
+ if trust is None:
+ token_values['trust_id'] = None
+ token_values['trustor_id'] = None
+ token_values['trustee_id'] = None
+ else:
+ token_values['trust_id'] = trust['id']
+ token_values['trustor_id'] = trust['trustor_id']
+ token_values['trustee_id'] = trust['trustee_id']
+
+ token_values['consumer_id'] = None
+ token_values['access_token_id'] = None
+
+ role_list = []
+ # Roles are by ID in metadata and by name in the user section
+ roles = access.get('metadata', {}).get('roles', [])
+ for role in roles:
+ role_list.append(role)
+ token_values['roles'] = role_list
+ return token_values
+
+
+def build_token_values(token_data):
+
+ token_expires_at = timeutils.parse_isotime(token_data['expires_at'])
+
+ # Trim off the microseconds because the revocation event only has
+ # expirations accurate to the second.
+ token_expires_at = token_expires_at.replace(microsecond=0)
+
+ token_values = {
+ 'expires_at': timeutils.normalize_time(token_expires_at),
+ 'issued_at': timeutils.normalize_time(
+ 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:
+ token_values['user_id'] = user['id']
+ # Federated users do not have a domain, be defensive and get the user
+ # domain set to None in the federated user case.
+ token_values['identity_domain_id'] = user.get('domain', {}).get('id')
+ else:
+ token_values['user_id'] = None
+ token_values['identity_domain_id'] = None
+
+ project = token_data.get('project', token_data.get('tenant'))
+ if project is not None:
+ token_values['project_id'] = project['id']
+ token_values['assignment_domain_id'] = project['domain']['id']
+ else:
+ token_values['project_id'] = None
+
+ domain = token_data.get('domain')
+ if domain is not None:
+ token_values['assignment_domain_id'] = domain['id']
+ else:
+ token_values['assignment_domain_id'] = None
+
+ role_list = []
+ roles = token_data.get('roles')
+ if roles is not None:
+ for role in roles:
+ role_list.append(role['id'])
+ token_values['roles'] = role_list
+
+ trust = token_data.get('OS-TRUST:trust')
+ if trust is None:
+ token_values['trust_id'] = None
+ token_values['trustor_id'] = None
+ token_values['trustee_id'] = None
+ else:
+ token_values['trust_id'] = trust['id']
+ token_values['trustor_id'] = trust['trustor_user']['id']
+ token_values['trustee_id'] = trust['trustee_user']['id']
+
+ oauth1 = token_data.get('OS-OAUTH1')
+ if oauth1 is None:
+ token_values['consumer_id'] = None
+ token_values['access_token_id'] = None
+ else:
+ token_values['consumer_id'] = oauth1['consumer_id']
+ token_values['access_token_id'] = oauth1['access_token_id']
+ return token_values
diff --git a/keystone-moon/keystone/contrib/revoke/routers.py b/keystone-moon/keystone/contrib/revoke/routers.py
new file mode 100644
index 00000000..4d2edfc0
--- /dev/null
+++ b/keystone-moon/keystone/contrib/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.contrib.revoke import controllers
+
+
+class RevokeExtension(wsgi.V3ExtensionRouter):
+
+ 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'))