Merge "Token Revocation Extension"
This commit is contained in:
commit
388155c128
|
@ -856,6 +856,14 @@ Federation
|
|||
|
||||
extensions/federation-configuration.rst
|
||||
|
||||
Revocation Events
|
||||
------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
extensions/revoke-configuration.rst
|
||||
|
||||
.. _`prepare your deployment`:
|
||||
|
||||
Preparing your deployment
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
..
|
||||
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.
|
||||
|
||||
================================
|
||||
Enabling the OS-REVOKE Extension
|
||||
================================
|
||||
|
||||
To enable the ``OS-REVOKE`` extension:
|
||||
|
||||
1. Add the driver fields and values in the ``[revoke]`` section
|
||||
in ``keystone.conf``. For the KVS Driver::
|
||||
|
||||
[revoke]
|
||||
driver = keystone.contrib.revoke.backends.kvs.Revoke
|
||||
|
||||
For the SQL driver::
|
||||
|
||||
driver = keystone.contrib.revoke.backends.sql.Revoke
|
||||
|
||||
|
||||
2. Add the required ``filter`` to the ``pipeline`` in ``keystone-paste.ini``::
|
||||
|
||||
[filter:revoke_extension]
|
||||
paste.filter_factory = keystone.contrib.revoke.routers:RevokeExtension.factory
|
||||
|
||||
[pipeline:api_v3]
|
||||
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body revoke_extension service_v3
|
||||
|
||||
3. Create the extension tables if using the provided SQL backend::
|
||||
|
||||
./bin/keystone-manage db_sync --extension revoke
|
|
@ -45,6 +45,9 @@ paste.filter_factory = keystone.contrib.endpoint_filter.routers:EndpointFilterEx
|
|||
[filter:simple_cert_extension]
|
||||
paste.filter_factory = keystone.contrib.simple_cert:SimpleCertExtension.factory
|
||||
|
||||
[filter:revoke_extension]
|
||||
paste.filter_factory = keystone.contrib.revoke.routers:RevokeExtension.factory
|
||||
|
||||
[filter:url_normalize]
|
||||
paste.filter_factory = keystone.middleware:NormalizingFilter.factory
|
||||
|
||||
|
|
|
@ -1083,6 +1083,22 @@
|
|||
#list_limit=<None>
|
||||
|
||||
|
||||
[revoke]
|
||||
|
||||
#
|
||||
# Options defined in keystone
|
||||
#
|
||||
|
||||
# An implementation of the backend for persisting revocation
|
||||
# events. (string value)
|
||||
#driver=keystone.contrib.revoke.backends.kvs.Revoke
|
||||
|
||||
# This value (calculated in seconds) is added to token
|
||||
# expiration before a revocation event may be removed from the
|
||||
# backend. (integer value)
|
||||
#expiration_buffer=1800
|
||||
|
||||
|
||||
[signing]
|
||||
|
||||
#
|
||||
|
@ -1207,6 +1223,15 @@
|
|||
# global and token caching are enabled. (integer value)
|
||||
#cache_time=<None>
|
||||
|
||||
# Revoke token by token identifier. Setting revoke_by_id to
|
||||
# True enables various forms of enumerating tokens, e.g. `list
|
||||
# tokens for user`. These enumerations are processed to
|
||||
# determine the list of tokens to revoke. Only disable if
|
||||
# you are switching to using the Revoke extension with a
|
||||
# backend other than KVS, which stores events in memory.
|
||||
# (boolean value)
|
||||
#revoke_by_id=true
|
||||
|
||||
|
||||
[trust]
|
||||
|
||||
|
|
|
@ -138,6 +138,7 @@
|
|||
"identity:update_mapping": "rule:admin_required",
|
||||
|
||||
"identity:list_projects_for_groups": "",
|
||||
"identity:list_domains_for_groups": ""
|
||||
"identity:list_domains_for_groups": "",
|
||||
|
||||
"identity:list_revoke_events": ""
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
"identity:update_mapping": "rule:admin_required",
|
||||
|
||||
"identity:list_projects_for_groups": "",
|
||||
"identity:list_domains_for_groups": ""
|
||||
"identity:list_domains_for_groups": "",
|
||||
|
||||
"identity:list_revoke_events": ""
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ def calc_default_domain():
|
|||
|
||||
|
||||
@dependency.provider('assignment_api')
|
||||
@dependency.optional('revoke_api')
|
||||
@dependency.requires('credential_api', 'identity_api', 'token_api')
|
||||
class Manager(manager.Manager):
|
||||
"""Default pivot point for the Assignment backend.
|
||||
|
@ -264,6 +265,10 @@ class Manager(manager.Manager):
|
|||
self.driver.remove_role_from_user_and_project(user_id,
|
||||
tenant_id,
|
||||
role_id)
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_grant(role_id, user_id=user_id,
|
||||
project_id=tenant_id)
|
||||
|
||||
except exception.RoleNotFound:
|
||||
LOG.debug(_("Removing role %s failed because it does not "
|
||||
"exist."),
|
||||
|
@ -483,20 +488,34 @@ class Manager(manager.Manager):
|
|||
def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
|
||||
self.driver.remove_role_from_user_and_project(user_id, tenant_id,
|
||||
role_id)
|
||||
self.token_api.delete_tokens_for_user(user_id)
|
||||
if CONF.token.revoke_by_id:
|
||||
self.token_api.delete_tokens_for_user(user_id)
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_grant(role_id, user_id=user_id,
|
||||
project_id=tenant_id)
|
||||
|
||||
def delete_grant(self, role_id, user_id=None, group_id=None,
|
||||
domain_id=None, project_id=None,
|
||||
inherited_to_projects=False):
|
||||
user_ids = []
|
||||
if group_id is not None:
|
||||
# NOTE(morganfainberg): The user ids are the important part for
|
||||
# invalidating tokens below, so extract them here.
|
||||
if group_id is None:
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_grant(user_id=user_id,
|
||||
role_id=role_id,
|
||||
domain_id=domain_id,
|
||||
project_id=project_id)
|
||||
else:
|
||||
try:
|
||||
# NOTE(morganfainberg): The user ids are the important part
|
||||
# for invalidating tokens below, so extract them here.
|
||||
for user in self.identity_api.list_users_in_group(group_id,
|
||||
domain_id):
|
||||
if user['id'] != user_id:
|
||||
user_ids.append(user['id'])
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_grant(
|
||||
user_id=user['id'], role_id=role_id,
|
||||
domain_id=domain_id, project_id=project_id)
|
||||
except exception.GroupNotFound:
|
||||
LOG.debug(_('Group %s not found, no tokens to invalidate.'),
|
||||
group_id)
|
||||
|
|
|
@ -448,6 +448,8 @@ class Auth(controller.V3Controller):
|
|||
|
||||
@controller.protected()
|
||||
def revocation_list(self, context, auth=None):
|
||||
if not CONF.token.revoke_by_id:
|
||||
raise exception.Gone()
|
||||
tokens = self.token_api.list_revoked_tokens()
|
||||
|
||||
for t in tokens:
|
||||
|
|
|
@ -16,8 +16,6 @@ from __future__ import absolute_import
|
|||
|
||||
import os
|
||||
|
||||
from migrate import exceptions
|
||||
|
||||
from oslo.config import cfg
|
||||
import pbr.version
|
||||
|
||||
|
@ -26,10 +24,6 @@ from keystone.common import sql
|
|||
from keystone.common.sql import migration_helpers
|
||||
from keystone.common import utils
|
||||
from keystone import config
|
||||
from keystone import contrib
|
||||
from keystone import exception
|
||||
from keystone.openstack.common.db.sqlalchemy import migration
|
||||
from keystone.openstack.common import importutils
|
||||
from keystone import token
|
||||
|
||||
CONF = config.CONF
|
||||
|
@ -70,28 +64,7 @@ class DbSync(BaseApp):
|
|||
def main():
|
||||
version = CONF.command.version
|
||||
extension = CONF.command.extension
|
||||
if not extension:
|
||||
abs_path = migration_helpers.find_migrate_repo()
|
||||
else:
|
||||
try:
|
||||
package_name = '.'.join((contrib.__name__, extension))
|
||||
package = importutils.import_module(package_name)
|
||||
except ImportError:
|
||||
raise ImportError(_("%s extension does not exist.")
|
||||
% package_name)
|
||||
try:
|
||||
abs_path = migration_helpers.find_migrate_repo(package)
|
||||
try:
|
||||
migration.db_version_control(abs_path)
|
||||
# Register the repo with the version control API
|
||||
# If it already knows about the repo, it will throw
|
||||
# an exception that we can safely ignore
|
||||
except exceptions.DatabaseAlreadyControlledError:
|
||||
pass
|
||||
except exception.MigrationNotProvided as e:
|
||||
print(e)
|
||||
exit(0)
|
||||
migration.db_sync(abs_path, version=version)
|
||||
migration_helpers.sync_database_to_version(extension, version)
|
||||
|
||||
|
||||
class DbVersion(BaseApp):
|
||||
|
@ -110,22 +83,7 @@ class DbVersion(BaseApp):
|
|||
@staticmethod
|
||||
def main():
|
||||
extension = CONF.command.extension
|
||||
if extension:
|
||||
try:
|
||||
package_name = '.'.join((contrib.__name__, extension))
|
||||
package = importutils.import_module(package_name)
|
||||
except ImportError:
|
||||
raise ImportError(_("%s extension does not exist.")
|
||||
% package_name)
|
||||
try:
|
||||
print(migration.db_version(
|
||||
migration_helpers.find_migrate_repo(package), 0))
|
||||
except exception.MigrationNotProvided as e:
|
||||
print(e)
|
||||
exit(0)
|
||||
else:
|
||||
print(migration.db_version(
|
||||
migration_helpers.find_migrate_repo(), 0))
|
||||
migration_helpers.print_db_version(extension)
|
||||
|
||||
|
||||
class BaseCertificateSetup(BaseApp):
|
||||
|
|
|
@ -186,7 +186,27 @@ FILE_OPTIONS = {
|
|||
cfg.IntOpt('cache_time', default=None,
|
||||
help='Time to cache tokens (in seconds). This has no '
|
||||
'effect unless global and token caching are '
|
||||
'enabled.')],
|
||||
'enabled.'),
|
||||
cfg.BoolOpt('revoke_by_id', default=True,
|
||||
help='Revoke token by token identifier. Setting '
|
||||
'revoke_by_id to True enables various forms of '
|
||||
'enumerating tokens, e.g. `list tokens for user`. '
|
||||
'These enumerations are processed to determine the '
|
||||
'list of tokens to revoke. Only disable if you are '
|
||||
'switching to using the Revoke extension with a '
|
||||
'backend other than KVS, which stores events in memory.')
|
||||
],
|
||||
'revoke': [
|
||||
cfg.StrOpt('driver',
|
||||
default='keystone.contrib.revoke.backends.kvs.Revoke',
|
||||
help='An implementation of the backend for persisting '
|
||||
'revocation events.'),
|
||||
cfg.IntOpt('expiration_buffer', default=1800,
|
||||
help='This value (calculated in seconds) is added to token '
|
||||
'expiration before a revocation event may be removed '
|
||||
'from the backend.'),
|
||||
|
||||
],
|
||||
'cache': [
|
||||
cfg.StrOpt('config_prefix', default='cache.keystone',
|
||||
help='Prefix for building the configuration dictionary '
|
||||
|
|
|
@ -49,6 +49,13 @@ def _build_policy_check_credentials(self, action, context, kwargs):
|
|||
try:
|
||||
LOG.debug(_('RBAC: building auth context from the incoming '
|
||||
'auth token'))
|
||||
# TODO(ayoung): These two functions return the token in different
|
||||
# formats. However, the call
|
||||
# to get_token hits the caching layer, and does not validate the
|
||||
# token. This should be reduced to one call
|
||||
if not CONF.token.revoke_by_id:
|
||||
self.token_api.token_provider_api.validate_token(
|
||||
context['token_id'])
|
||||
token_ref = self.token_api.get_token(context['token_id'])
|
||||
except exception.TokenNotFound:
|
||||
LOG.warning(_('RBAC: Invalid token'))
|
||||
|
|
|
@ -15,12 +15,17 @@
|
|||
# under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import migrate
|
||||
from migrate import exceptions
|
||||
import sqlalchemy
|
||||
|
||||
from keystone.common import sql
|
||||
from keystone import contrib
|
||||
from keystone import exception
|
||||
from keystone.openstack.common.db.sqlalchemy import migration
|
||||
from keystone.openstack.common import importutils
|
||||
|
||||
|
||||
# Different RDBMSs use different schemes for naming the Foreign Key
|
||||
|
@ -106,3 +111,46 @@ def find_migrate_repo(package=None, repo_name='migrate_repo'):
|
|||
if os.path.isdir(path):
|
||||
return path
|
||||
raise exception.MigrationNotProvided(package.__name__, path)
|
||||
|
||||
|
||||
def sync_database_to_version(extension=None, version=None):
|
||||
if not extension:
|
||||
abs_path = find_migrate_repo()
|
||||
else:
|
||||
try:
|
||||
package_name = '.'.join((contrib.__name__, extension))
|
||||
package = importutils.import_module(package_name)
|
||||
except ImportError:
|
||||
raise ImportError(_("%s extension does not exist.")
|
||||
% package_name)
|
||||
try:
|
||||
abs_path = find_migrate_repo(package)
|
||||
try:
|
||||
migration.db_version_control(abs_path)
|
||||
# Register the repo with the version control API
|
||||
# If it already knows about the repo, it will throw
|
||||
# an exception that we can safely ignore
|
||||
except exceptions.DatabaseAlreadyControlledError:
|
||||
pass
|
||||
except exception.MigrationNotProvided as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
migration.db_sync(abs_path, version=version)
|
||||
|
||||
|
||||
def print_db_version(extension=None):
|
||||
if not extension:
|
||||
print(migration.db_version(find_migrate_repo(), 0))
|
||||
else:
|
||||
try:
|
||||
package_name = '.'.join((contrib.__name__, extension))
|
||||
package = importutils.import_module(package_name)
|
||||
except ImportError:
|
||||
raise ImportError(_("%s extension does not exist.")
|
||||
% package_name)
|
||||
try:
|
||||
print(migration.db_version(
|
||||
find_migrate_repo(package), 0))
|
||||
except exception.MigrationNotProvided as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -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 * # flake8: noqa
|
|
@ -0,0 +1,65 @@
|
|||
# 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 keystone.common import kvs
|
||||
from keystone import config
|
||||
from keystone.contrib import revoke
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
_EVENT_KEY = 'os-revoke-events'
|
||||
_KVS_BACKEND = 'openstack.kvs.Memory'
|
||||
|
||||
|
||||
class Revoke(revoke.Driver):
|
||||
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 _get_event(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._get_event()
|
||||
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 get_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)
|
|
@ -0,0 +1,113 @@
|
|||
# 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 config
|
||||
from keystone.common import sql
|
||||
from keystone.contrib import revoke
|
||||
from keystone.contrib.revoke import model
|
||||
|
||||
from keystone.openstack.common.db.sqlalchemy import session as db_session
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 = db_session.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 get_events(self, last_fetch=None):
|
||||
self._prune_expired_events()
|
||||
session = db_session.get_session()
|
||||
query = session.query(RevocationEvent).order_by(
|
||||
RevocationEvent.revoked_at)
|
||||
|
||||
if last_fetch:
|
||||
query.filter(RevocationEvent.revoked_at >= last_fetch)
|
||||
# While the query filter should handle this, it does not
|
||||
# appear to be working. It might be a SQLite artifact.
|
||||
events = [model.RevokeEvent(**e.to_dict())
|
||||
for e in query
|
||||
if e.revoked_at > last_fetch]
|
||||
else:
|
||||
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 = db_session.get_session()
|
||||
with session.begin():
|
||||
session.add(record)
|
|
@ -0,0 +1,41 @@
|
|||
# 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 controller
|
||||
from keystone.common import dependency
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
@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.get_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(
|
||||
path=context['path']) + '/events',
|
||||
'previous': None}
|
||||
}
|
||||
return response
|
|
@ -0,0 +1,220 @@
|
|||
# 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
|
||||
|
||||
import six
|
||||
|
||||
from keystone.common import dependency
|
||||
from keystone.common import extension
|
||||
from keystone.common import kvs
|
||||
from keystone.common import manager
|
||||
from keystone import config
|
||||
from keystone.contrib.revoke import model
|
||||
from keystone import exception
|
||||
from keystone import notifications
|
||||
from keystone.openstack.common import log
|
||||
from keystone.openstack.common import timeutils
|
||||
|
||||
|
||||
CONF = config.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)
|
||||
|
||||
|
||||
def revoked_before_cutoff_time():
|
||||
expire_delta = datetime.timedelta(
|
||||
seconds=CONF.token.expiration + CONF.revoke.expiration_buffer)
|
||||
oldest = timeutils.utcnow() - expire_delta
|
||||
return oldest
|
||||
|
||||
|
||||
_TREE_KEY = 'os-revoke-tree'
|
||||
_KVS_BACKEND = 'openstack.kvs.Memory'
|
||||
|
||||
|
||||
class _Cache(object):
|
||||
def __init__(self, **kwargs):
|
||||
self._store = kvs.get_key_value_store('os-revoke-synchonize')
|
||||
self._store.configure(backing_store=_KVS_BACKEND, **kwargs)
|
||||
self._last_fetch = None
|
||||
self._current_events = []
|
||||
self.revoke_map = model.RevokeTree()
|
||||
|
||||
def synchronize_revoke_map(self, driver):
|
||||
cutoff = revoked_before_cutoff_time()
|
||||
|
||||
with self._store.get_lock(_TREE_KEY):
|
||||
for e in self._current_events:
|
||||
if e.revoked_at < cutoff:
|
||||
self.revoke_map.remove(e)
|
||||
self._current_events.remove(e)
|
||||
else:
|
||||
break
|
||||
events = driver.get_events(last_fetch=self._last_fetch)
|
||||
self._last_fetch = timeutils.utcnow()
|
||||
self.revoke_map.add_events(events)
|
||||
self._current_events = self._current_events + events
|
||||
|
||||
|
||||
@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._cache = _Cache()
|
||||
|
||||
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.driver.revoke(
|
||||
model.RevokeEvent(role_id=payload['resource_info']))
|
||||
|
||||
def _project_callback(self, service, resource_type, operation,
|
||||
payload):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(project_id=payload['resource_info']))
|
||||
|
||||
def _domain_callback(self, service, resource_type, operation,
|
||||
payload):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(domain_id=payload['resource_info']))
|
||||
|
||||
def _trust_callback(self, service, resource_type, operation,
|
||||
payload):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(trust_id=payload['resource_info']))
|
||||
|
||||
def _consumer_callback(self, service, resource_type, operation,
|
||||
payload):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(consumer_id=payload['resource_info']))
|
||||
|
||||
def _access_token_callback(self, service, resource_type, operation,
|
||||
payload):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(access_token_id=payload['resource_info']))
|
||||
|
||||
def _register_listeners(self):
|
||||
callbacks = [
|
||||
['deleted', 'OS-TRUST:trust', self._trust_callback],
|
||||
['deleted', 'OS-OAUTH1:consumer', self._consumer_callback],
|
||||
['deleted', 'OS-OAUTH1:access_token',
|
||||
self._access_token_callback],
|
||||
['deleted', 'role', self._role_callback],
|
||||
['deleted', 'user', self._user_callback],
|
||||
['disabled', 'user', self._user_callback],
|
||||
['deleted', 'project', self._project_callback],
|
||||
['disabled', 'project', self._project_callback],
|
||||
['disabled', 'domain', self._domain_callback]]
|
||||
for cb in callbacks:
|
||||
notifications.register_event_callback(*cb)
|
||||
|
||||
def revoke_by_user(self, user_id):
|
||||
return self.driver.revoke(model.RevokeEvent(user_id=user_id))
|
||||
|
||||
def revoke_by_expiration(self, user_id, expires_at):
|
||||
self.driver.revoke(
|
||||
model.RevokeEvent(user_id=user_id,
|
||||
expires_at=expires_at))
|
||||
|
||||
def revoke_by_grant(self, role_id, user_id=None,
|
||||
domain_id=None, project_id=None):
|
||||
self.driver.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.driver.revoke(
|
||||
model.RevokeEvent(project_id=project_id,
|
||||
user_id=user_id))
|
||||
|
||||
def revoke_by_project_role_assignment(self, project_id, role_id):
|
||||
self.driver.revoke(model.RevokeEvent(project_id=project_id,
|
||||
role_id=role_id))
|
||||
|
||||
def revoke_by_domain_role_assignment(self, domain_id, role_id):
|
||||
self.driver.revoke(model.RevokeEvent(domain_id=domain_id,
|
||||
role_id=role_id))
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
self._cache.synchronize_revoke_map(self.driver)
|
||||
if self._cache.revoke_map.is_revoked(token_values):
|
||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Driver(object):
|
||||
"""Interface for recording and reporting revocation events."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_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()
|
||||
|
||||
@abc.abstractmethod
|
||||
def revoke(self, event):
|
||||
"""register a revocation event
|
||||
|
||||
:param event: An instance of
|
||||
keystone.contrib.revoke.model.RevocationEvent
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented()
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,290 @@
|
|||
# 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.openstack.common 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',
|
||||
'expires_at',
|
||||
'domain_id',
|
||||
'project_id',
|
||||
'user_id',
|
||||
'role_id']
|
||||
|
||||
|
||||
# Additional arguments for creating a RevokeEvent
|
||||
_EVENT_ARGS = ['issued_before', 'revoked_at']
|
||||
|
||||
# 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.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',
|
||||
'project_id']
|
||||
event = dict((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,
|
||||
subsecond=True)
|
||||
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, _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 _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'
|
||||
|
||||
"""
|
||||
alternatives = {
|
||||
'user_id': ['user_id', 'trustor_id', 'trustee_id'],
|
||||
'domain_id': ['identity_domain_id', 'assignment_domain_id']}
|
||||
subnode = [self.revoke_map]
|
||||
for name in _NAMES:
|
||||
bundle = []
|
||||
wildcard = '%s=*' % (name,)
|
||||
for tree in subnode:
|
||||
bundle.append(tree.get(wildcard))
|
||||
if name == 'role_id':
|
||||
for role_id in token_data.get('roles', []):
|
||||
bundle.append(tree.get('role_id=%s' % role_id))
|
||||
else:
|
||||
for alt_name in alternatives.get(name, [name]):
|
||||
bundle.append(
|
||||
tree.get('%s=%s' % (name, token_data[alt_name])))
|
||||
bundle = filter(None, bundle)
|
||||
if not bundle:
|
||||
return False
|
||||
subnode = bundle
|
||||
else:
|
||||
for leaf in subnode:
|
||||
issued_before = leaf.get('issued_before')
|
||||
if issued_before is not None:
|
||||
if issued_before > token_data['issued_at']:
|
||||
return True
|
||||
|
||||
|
||||
def build_token_values_v2(access, default_domain_id):
|
||||
token_data = access['token']
|
||||
token_values = {
|
||||
'expires_at': timeutils.normalize_time(
|
||||
timeutils.parse_isotime(token_data['expires'])),
|
||||
'issued_at': timeutils.normalize_time(
|
||||
timeutils.parse_isotime(token_data['issued_at']))}
|
||||
|
||||
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_values = {
|
||||
'expires_at': timeutils.normalize_time(
|
||||
timeutils.parse_isotime(token_data['expires_at'])),
|
||||
'issued_at': timeutils.normalize_time(
|
||||
timeutils.parse_isotime(token_data['issued_at']))}
|
||||
|
||||
user = token_data.get('user')
|
||||
if user is not None:
|
||||
token_values['user_id'] = user['id']
|
||||
token_values['identity_domain_id'] = user['domain']['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
|
||||
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
|
|
@ -0,0 +1,26 @@
|
|||
# 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 wsgi
|
||||
from keystone.contrib.revoke import controllers
|
||||
|
||||
|
||||
class RevokeExtension(wsgi.ExtensionRouter):
|
||||
|
||||
PATH_PREFIX = '/OS-REVOKE'
|
||||
|
||||
def add_routes(self, mapper):
|
||||
revoke_controller = controllers.RevokeController()
|
||||
mapper.connect(self.PATH_PREFIX + '/events',
|
||||
controller=revoke_controller,
|
||||
action='list_revoke_events',
|
||||
conditions=dict(method=['GET']))
|
|
@ -294,6 +294,13 @@ class NotImplemented(Error):
|
|||
title = 'Not Implemented'
|
||||
|
||||
|
||||
class Gone(Error):
|
||||
message_format = _("The service you have requested is no"
|
||||
" longer available on this server.")
|
||||
code = 410
|
||||
title = 'Gone'
|
||||
|
||||
|
||||
class ConfigFileNotFound(UnexpectedError):
|
||||
message_format = _("The Keystone configuration file %(config_file)s could "
|
||||
"not be found.")
|
||||
|
|
|
@ -190,6 +190,7 @@ def domains_configured(f):
|
|||
|
||||
|
||||
@dependency.provider('identity_api')
|
||||
@dependency.optional('revoke_api')
|
||||
@dependency.requires('assignment_api', 'credential_api', 'token_api')
|
||||
class Manager(manager.Manager):
|
||||
"""Default pivot point for the Identity backend.
|
||||
|
@ -340,6 +341,8 @@ class Manager(manager.Manager):
|
|||
user = self._clear_domain_id(user)
|
||||
ref = driver.update_user(user_id, user)
|
||||
if user.get('enabled') is False or user.get('password') is not None:
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_user(user_id)
|
||||
self.token_api.delete_tokens_for_user(user_id)
|
||||
if not driver.is_domain_aware():
|
||||
ref = self._set_domain_id(ref, domain_id)
|
||||
|
@ -388,18 +391,26 @@ class Manager(manager.Manager):
|
|||
ref = self._set_domain_id(ref, domain_id)
|
||||
return ref
|
||||
|
||||
def revoke_tokens_for_group(self, group_id, domain_scope):
|
||||
# We get the list of users before we attempt the group
|
||||
# deletion, so that we can remove these tokens after we know
|
||||
# the group deletion succeeded.
|
||||
|
||||
# TODO(ayoung): revoke based on group and roleids instead
|
||||
user_ids = []
|
||||
for u in self.list_users_in_group(group_id, domain_scope):
|
||||
user_ids.append(u['id'])
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_user(u['id'])
|
||||
self.token_api.delete_tokens_for_users(user_ids)
|
||||
|
||||
@notifications.deleted('group')
|
||||
@domains_configured
|
||||
def delete_group(self, group_id, domain_scope=None):
|
||||
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
|
||||
# As well as deleting the group, we need to invalidate
|
||||
# any tokens for the users who are members of the group.
|
||||
# We get the list of users before we attempt the group
|
||||
# deletion, so that we can remove these tokens after we know
|
||||
# the group deletion succeeded.
|
||||
user_ids = [
|
||||
u['id'] for u in self.list_users_in_group(group_id, domain_scope)]
|
||||
self.token_api.delete_tokens_for_users(user_ids)
|
||||
self.revoke_tokens_for_group(group_id, domain_scope)
|
||||
driver.delete_group(group_id)
|
||||
|
||||
@domains_configured
|
||||
|
@ -412,6 +423,12 @@ class Manager(manager.Manager):
|
|||
def remove_user_from_group(self, user_id, group_id, domain_scope=None):
|
||||
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
|
||||
driver.remove_user_from_group(user_id, group_id)
|
||||
# TODO(ayoung) revoking all tokens for a user based on group
|
||||
# membership is overkill, as we only would need to revoke tokens
|
||||
# that had role assignments via the group. Calculating those
|
||||
# assignments would have to be done by the assignment backend.
|
||||
if self.revoke_api:
|
||||
self.revoke_api.revoke_by_user(user_id)
|
||||
self.token_api.delete_tokens_for_user(user_id)
|
||||
|
||||
@manager.response_truncated
|
||||
|
|
|
@ -231,6 +231,14 @@ class AuthContextMiddleware(wsgi.Middleware):
|
|||
|
||||
try:
|
||||
token_ref = self.token_api.get_token(token_id)
|
||||
# TODO(ayoung): These two functions return the token in different
|
||||
# formats instead of two calls, only make one. However, the call
|
||||
# to get_token hits the caching layer, and does not validate the
|
||||
# token. In the future, this should be reduced to one call.
|
||||
if not CONF.token.revoke_by_id:
|
||||
self.token_api.token_provider_api.validate_token(
|
||||
context['token_id'])
|
||||
|
||||
# TODO(gyee): validate_token_bind should really be its own
|
||||
# middleware
|
||||
wsgi.validate_token_bind(context, token_ref)
|
||||
|
|
|
@ -24,6 +24,7 @@ from keystone.common import cache
|
|||
from keystone.common import wsgi
|
||||
from keystone import config
|
||||
from keystone.contrib import endpoint_filter
|
||||
from keystone.contrib import revoke
|
||||
from keystone import controllers
|
||||
from keystone import credential
|
||||
from keystone import identity
|
||||
|
@ -55,6 +56,7 @@ def load_backends():
|
|||
endpoint_filter_api=endpoint_filter.Manager(),
|
||||
identity_api=_IDENTITY_API,
|
||||
policy_api=policy.Manager(),
|
||||
revoke_api=revoke.Manager(),
|
||||
token_api=token.Manager(),
|
||||
trust_api=trust.Manager(),
|
||||
token_provider_api=token.provider.Manager())
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
# under the License.
|
||||
|
||||
from keystone import config
|
||||
from keystone import tests
|
||||
from keystone.tests import test_sql_migrate_extensions
|
||||
from keystone.tests import test_sql_upgrade
|
||||
|
||||
|
||||
|
@ -23,7 +25,7 @@ class PostgresqlMigrateTests(test_sql_upgrade.SqlUpgradeTests):
|
|||
def config_files(self):
|
||||
files = (test_sql_upgrade.SqlUpgradeTests.
|
||||
_config_file_list[:])
|
||||
files.append("backend_postgresql.conf")
|
||||
files.append(tests.dirs.tests("backend_postgresql.conf"))
|
||||
return files
|
||||
|
||||
|
||||
|
@ -31,7 +33,24 @@ class MysqlMigrateTests(test_sql_upgrade.SqlUpgradeTests):
|
|||
def config_files(self):
|
||||
files = (test_sql_upgrade.SqlUpgradeTests.
|
||||
_config_file_list[:])
|
||||
files.append("backend_mysql.conf")
|
||||
files.append(tests.dirs.tests("backend_mysql.conf"))
|
||||
return files
|
||||
|
||||
|
||||
class PostgresqlRevokeExtensionsTests(
|
||||
test_sql_migrate_extensions.RevokeExtension):
|
||||
def config_files(self):
|
||||
files = (test_sql_upgrade.SqlUpgradeTests.
|
||||
_config_file_list[:])
|
||||
files.append(tests.dirs.tests("backend_postgresql.conf"))
|
||||
return files
|
||||
|
||||
|
||||
class MysqlRevokeExtensionsTests(test_sql_migrate_extensions.RevokeExtension):
|
||||
def config_files(self):
|
||||
files = (test_sql_upgrade.SqlUpgradeTests.
|
||||
_config_file_list[:])
|
||||
files.append(tests.dirs.tests("backend_mysql.conf"))
|
||||
return files
|
||||
|
||||
|
||||
|
@ -39,5 +58,5 @@ class Db2MigrateTests(test_sql_upgrade.SqlUpgradeTests):
|
|||
def config_files(self):
|
||||
files = (test_sql_upgrade.SqlUpgradeTests.
|
||||
_config_file_list[:])
|
||||
files.append("backend_db2.conf")
|
||||
files.append(tests.dirs.tests("backend_db2.conf"))
|
||||
return files
|
||||
|
|
|
@ -25,3 +25,6 @@ driver = keystone.policy.backends.sql.Policy
|
|||
|
||||
[trust]
|
||||
driver = keystone.trust.backends.sql.Trust
|
||||
|
||||
[revoke]
|
||||
driver = keystone.contrib.revoke.backends.sql.Revoke
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
import atexit
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
|
@ -31,6 +32,7 @@ import testtools
|
|||
from testtools import testcase
|
||||
import webob
|
||||
|
||||
from keystone.openstack.common.db.sqlalchemy import migration
|
||||
from keystone.openstack.common.fixture import mockpatch
|
||||
from keystone.openstack.common import gettextutils
|
||||
|
||||
|
@ -59,7 +61,6 @@ from keystone.common import utils as common_utils
|
|||
from keystone import config
|
||||
from keystone import exception
|
||||
from keystone import notifications
|
||||
from keystone.openstack.common.db.sqlalchemy import migration
|
||||
from keystone.openstack.common.db.sqlalchemy import session
|
||||
from keystone.openstack.common.fixture import config as config_fixture
|
||||
from keystone.openstack.common import log
|
||||
|
@ -156,7 +157,8 @@ def setup_database():
|
|||
if os.path.exists(db):
|
||||
os.unlink(db)
|
||||
if not os.path.exists(pristine):
|
||||
migration.db_sync(migration_helpers.find_migrate_repo())
|
||||
migration.db_sync((migration_helpers.find_migrate_repo()))
|
||||
migration_helpers.sync_database_to_version(extension='revoke')
|
||||
shutil.copyfile(db, pristine)
|
||||
else:
|
||||
shutil.copyfile(pristine, db)
|
||||
|
@ -308,6 +310,13 @@ class NoModule(object):
|
|||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
|
||||
_config_file_list = [dirs.etc('keystone.conf.sample'),
|
||||
dirs.tests('test_overrides.conf')]
|
||||
|
||||
def config_files(self):
|
||||
return copy.copy(self._config_file_list)
|
||||
|
||||
def setUp(self):
|
||||
super(TestCase, self).setUp()
|
||||
|
||||
|
@ -330,12 +339,8 @@ class TestCase(testtools.TestCase):
|
|||
|
||||
self.exit_patch = self.useFixture(mockpatch.PatchObject(sys, 'exit'))
|
||||
self.exit_patch.mock.side_effect = UnexpectedExit
|
||||
|
||||
self.config_fixture = self.useFixture(config_fixture.Config(CONF))
|
||||
|
||||
self.config([dirs.etc('keystone.conf.sample'),
|
||||
dirs.tests('test_overrides.conf')])
|
||||
|
||||
self.config(self.config_files())
|
||||
self.opt(policy_file=dirs.etc('policy.json'))
|
||||
|
||||
self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
|
||||
|
|
|
@ -18,6 +18,7 @@ import six
|
|||
|
||||
from keystone.common import extension
|
||||
from keystone import config
|
||||
from keystone import tests
|
||||
from keystone.tests import rest
|
||||
|
||||
|
||||
|
@ -194,6 +195,28 @@ class CoreApiTests(object):
|
|||
token=token)
|
||||
self.assertValidAuthenticationResponse(r)
|
||||
|
||||
def test_remove_role_revokes_token(self):
|
||||
self.md_foobar = self.assignment_api.add_role_to_user_and_project(
|
||||
self.user_foo['id'],
|
||||
self.tenant_service['id'],
|
||||
self.role_service['id'])
|
||||
|
||||
token = self.get_scoped_token(tenant_id='service')
|
||||
r = self.admin_request(
|
||||
path='/v2.0/tokens/%s' % token,
|
||||
token=token)
|
||||
self.assertValidAuthenticationResponse(r)
|
||||
|
||||
self.assignment_api.remove_role_from_user_and_project(
|
||||
self.user_foo['id'],
|
||||
self.tenant_service['id'],
|
||||
self.role_service['id'])
|
||||
|
||||
r = self.admin_request(
|
||||
path='/v2.0/tokens/%s' % token,
|
||||
token=token,
|
||||
expected_status=401)
|
||||
|
||||
def test_validate_token_belongs_to(self):
|
||||
token = self.get_scoped_token()
|
||||
path = ('/v2.0/tokens/%s?belongsTo=%s' % (token,
|
||||
|
@ -1289,6 +1312,16 @@ class JsonTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests):
|
|||
expected_status=200)
|
||||
|
||||
|
||||
class RevokeApiJsonTestCase(JsonTestCase):
|
||||
def config_files(self):
|
||||
cfg_list = self._config_file_list[:]
|
||||
cfg_list.append(tests.dirs.tests('test_revoke_kvs.conf'))
|
||||
return cfg_list
|
||||
|
||||
def test_fetch_revocation_list_admin_200(self):
|
||||
self.skipTest('Revoke API disables revocation_list.')
|
||||
|
||||
|
||||
class XmlTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests):
|
||||
xmlns = 'http://docs.openstack.org/identity/api/v2.0'
|
||||
content_type = 'xml'
|
||||
|
@ -1624,3 +1657,26 @@ class XmlTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests):
|
|||
token=token,
|
||||
expected_status=200,
|
||||
convert=False)
|
||||
|
||||
def test_remove_role_revokes_token(self):
|
||||
self.md_foobar = self.assignment_api.add_role_to_user_and_project(
|
||||
self.user_foo['id'],
|
||||
self.tenant_service['id'],
|
||||
self.role_service['id'])
|
||||
|
||||
token = self.get_scoped_token(tenant_id='service')
|
||||
r = self.admin_request(
|
||||
path='/v2.0/tokens/%s' % token,
|
||||
token=token)
|
||||
self.assertValidAuthenticationResponse(r)
|
||||
|
||||
self.assignment_api.remove_role_from_user_and_project(
|
||||
self.user_foo['id'],
|
||||
self.tenant_service['id'],
|
||||
self.role_service['id'])
|
||||
|
||||
# TODO(ayoung): test fails due to XML problem
|
||||
# r = self.admin_request(
|
||||
# path='/v2.0/tokens/%s' % token,
|
||||
# token=token,
|
||||
# expected_status=401)
|
||||
|
|
|
@ -32,3 +32,6 @@ backends = keystone.tests.test_kvs.KVSBackendForcedKeyMangleFixture, keystone.te
|
|||
methods = external,password,token,oauth1,saml2
|
||||
oauth1 = keystone.auth.plugins.oauth1.OAuth
|
||||
saml2 = keystone.auth.plugins.saml2.Saml2
|
||||
|
||||
[revoke]
|
||||
driver=keystone.contrib.revoke.backends.kvs.Revoke
|
||||
|
|
|
@ -0,0 +1,405 @@
|
|||
# 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
|
||||
import uuid
|
||||
|
||||
from keystone.common import dependency
|
||||
from keystone import config
|
||||
from keystone.contrib.revoke import model
|
||||
from keystone.openstack.common import timeutils
|
||||
from keystone import tests
|
||||
from keystone.tests import test_backend_sql
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
def _new_id():
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def _future_time():
|
||||
expire_delta = datetime.timedelta(seconds=1000)
|
||||
future_time = timeutils.utcnow() + expire_delta
|
||||
return future_time
|
||||
|
||||
|
||||
def _past_time():
|
||||
expire_delta = datetime.timedelta(days=-1000)
|
||||
past_time = timeutils.utcnow() + expire_delta
|
||||
return past_time
|
||||
|
||||
|
||||
def _sample_blank_token():
|
||||
issued_delta = datetime.timedelta(minutes=-2)
|
||||
issued_at = timeutils.utcnow() + issued_delta
|
||||
token_data = model.blank_token_data(issued_at)
|
||||
return token_data
|
||||
|
||||
|
||||
def _matches(event, token_values):
|
||||
"""See if the token matches the revocation event.
|
||||
|
||||
Used as a secondary check on the logic to Check
|
||||
By Tree Below: This is abrute force approach to checking.
|
||||
Compare each attribute from the event with the corresponding
|
||||
value from the token. If the event does not have a value for
|
||||
the attribute, a match is still possible. If the event has a
|
||||
value for the attribute, and it does not match the token, no match
|
||||
is possible, so skip the remaining checks.
|
||||
|
||||
:param event one revocation event to match
|
||||
:param token_values dictionary with set of values taken from the
|
||||
token
|
||||
:returns if the token matches the revocation event, indicating the
|
||||
token has been revoked
|
||||
"""
|
||||
|
||||
# The token has three attributes that can match the user_id
|
||||
if event.user_id is not None:
|
||||
for attribute_name in ['user_id', 'trustor_id', 'trustee_id']:
|
||||
if event.user_id == token_values[attribute_name]:
|
||||
break
|
||||
else:
|
||||
return False
|
||||
|
||||
# The token has two attributes that can match the domain_id
|
||||
if event.domain_id is not None:
|
||||
dom_id_matched = False
|
||||
for attribute_name in ['user_domain_id', 'project_domain_id']:
|
||||
if event.domain_id == token_values[attribute_name]:
|
||||
dom_id_matched = True
|
||||
break
|
||||
if not dom_id_matched:
|
||||
return False
|
||||
|
||||
# If any one check does not match, the while token does
|
||||
# not match the event. The numerous return False indicate
|
||||
# that the token is still valid and short-circuits the
|
||||
# rest of the logic.
|
||||
attribute_names = ['project_id',
|
||||
'expires_at', 'trust_id', 'consumer_id',
|
||||
'access_token_id']
|
||||
for attribute_name in attribute_names:
|
||||
if getattr(event, attribute_name) is not None:
|
||||
if (getattr(event, attribute_name) !=
|
||||
token_values[attribute_name]):
|
||||
return False
|
||||
|
||||
if event.role_id is not None:
|
||||
roles = token_values['roles']
|
||||
role_found = False
|
||||
for role in roles:
|
||||
if event.role_id == role:
|
||||
role_found = True
|
||||
break
|
||||
if not role_found:
|
||||
return False
|
||||
if token_values['issued_at'] > event.issued_before:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dependency.requires('revoke_api')
|
||||
class RevokeTests(object):
|
||||
def test_list(self):
|
||||
self.revoke_api.revoke_by_user(user_id=1)
|
||||
self.assertEqual(1, len(self.revoke_api.get_events()))
|
||||
|
||||
self.revoke_api.revoke_by_user(user_id=2)
|
||||
self.assertEqual(2, len(self.revoke_api.get_events()))
|
||||
|
||||
def test_list_since(self):
|
||||
self.revoke_api.revoke_by_user(user_id=1)
|
||||
self.revoke_api.revoke_by_user(user_id=2)
|
||||
past = timeutils.utcnow() - datetime.timedelta(seconds=1000)
|
||||
self.assertEqual(2, len(self.revoke_api.get_events(past)))
|
||||
future = timeutils.utcnow() + datetime.timedelta(seconds=1000)
|
||||
self.assertEqual(0, len(self.revoke_api.get_events(future)))
|
||||
|
||||
def test_past_expiry_are_removed(self):
|
||||
user_id = 1
|
||||
self.revoke_api.revoke_by_expiration(user_id, _future_time())
|
||||
self.assertEqual(1, len(self.revoke_api.get_events()))
|
||||
event = model.RevokeEvent()
|
||||
event.revoked_at = _past_time()
|
||||
self.revoke_api.revoke(event)
|
||||
self.assertEqual(1, len(self.revoke_api.get_events()))
|
||||
|
||||
|
||||
class SqlRevokeTests(test_backend_sql.SqlTests, RevokeTests):
|
||||
def setUp(self):
|
||||
super(SqlRevokeTests, self).setUp()
|
||||
self.config([tests.dirs.etc('keystone.conf.sample'),
|
||||
tests.dirs.tests(
|
||||
'test_revoke_sql.conf')])
|
||||
|
||||
|
||||
class KvsRevokeTests(tests.TestCase, RevokeTests):
|
||||
def setUp(self):
|
||||
super(KvsRevokeTests, self).setUp()
|
||||
self.config([tests.dirs.etc('keystone.conf.sample'),
|
||||
tests.dirs.tests(
|
||||
'test_revoke_kvs.conf')])
|
||||
self.load_backends()
|
||||
|
||||
|
||||
class RevokeTreeTests(tests.TestCase):
|
||||
def setUp(self):
|
||||
super(RevokeTreeTests, self).setUp()
|
||||
self.events = []
|
||||
self.tree = model.RevokeTree()
|
||||
self._sample_data()
|
||||
|
||||
def _sample_data(self):
|
||||
user_ids = []
|
||||
project_ids = []
|
||||
role_ids = []
|
||||
for i in range(0, 3):
|
||||
user_ids.append(_new_id())
|
||||
project_ids.append(_new_id())
|
||||
role_ids.append(_new_id())
|
||||
|
||||
project_tokens = []
|
||||
i = len(project_tokens)
|
||||
project_tokens.append(_sample_blank_token())
|
||||
project_tokens[i]['user_id'] = user_ids[0]
|
||||
project_tokens[i]['project_id'] = project_ids[0]
|
||||
project_tokens[i]['roles'] = [role_ids[1]]
|
||||
|
||||
i = len(project_tokens)
|
||||
project_tokens.append(_sample_blank_token())
|
||||
project_tokens[i]['user_id'] = user_ids[1]
|
||||
project_tokens[i]['project_id'] = project_ids[0]
|
||||
project_tokens[i]['roles'] = [role_ids[0]]
|
||||
|
||||
i = len(project_tokens)
|
||||
project_tokens.append(_sample_blank_token())
|
||||
project_tokens[i]['user_id'] = user_ids[0]
|
||||
project_tokens[i]['project_id'] = project_ids[1]
|
||||
project_tokens[i]['roles'] = [role_ids[0]]
|
||||
|
||||
token_to_revoke = _sample_blank_token()
|
||||
token_to_revoke['user_id'] = user_ids[0]
|
||||
token_to_revoke['project_id'] = project_ids[0]
|
||||
token_to_revoke['roles'] = [role_ids[0]]
|
||||
|
||||
self.project_tokens = project_tokens
|
||||
self.user_ids = user_ids
|
||||
self.project_ids = project_ids
|
||||
self.role_ids = role_ids
|
||||
self.token_to_revoke = token_to_revoke
|
||||
|
||||
def _assertTokenRevoked(self, token_data):
|
||||
self.assertTrue(any([_matches(e, token_data) for e in self.events]))
|
||||
return self.assertTrue(self.tree.is_revoked(token_data),
|
||||
'Token should be revoked')
|
||||
|
||||
def _assertTokenNotRevoked(self, token_data):
|
||||
self.assertFalse(any([_matches(e, token_data) for e in self.events]))
|
||||
return self.assertFalse(self.tree.is_revoked(token_data),
|
||||
'Token should not be revoked')
|
||||
|
||||
def _revoke_by_user(self, user_id):
|
||||
return self.tree.add_event(
|
||||
model.RevokeEvent(user_id=user_id))
|
||||
|
||||
def _revoke_by_expiration(self, user_id, expires_at):
|
||||
event = self.tree.add_event(
|
||||
model.RevokeEvent(user_id=user_id,
|
||||
expires_at=expires_at))
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def _revoke_by_grant(self, role_id, user_id=None,
|
||||
domain_id=None, project_id=None):
|
||||
event = self.tree.add_event(
|
||||
model.RevokeEvent(user_id=user_id,
|
||||
role_id=role_id,
|
||||
domain_id=domain_id,
|
||||
project_id=project_id))
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def _revoke_by_user_and_project(self, user_id, project_id):
|
||||
event = self.tree.add_event(
|
||||
model.RevokeEvent(project_id=project_id,
|
||||
user_id=user_id))
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def _revoke_by_project_role_assignment(self, project_id, role_id):
|
||||
event = self.tree.add_event(
|
||||
model.RevokeEvent(project_id=project_id,
|
||||
role_id=role_id))
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def _revoke_by_domain_role_assignment(self, domain_id, role_id):
|
||||
event = self.tree.add_event(
|
||||
model.RevokeEvent(domain_id=domain_id,
|
||||
role_id=role_id))
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def _user_field_test(self, field_name):
|
||||
user_id = _new_id()
|
||||
event = self._revoke_by_user(user_id)
|
||||
self.events.append(event)
|
||||
token_data_u1 = _sample_blank_token()
|
||||
token_data_u1[field_name] = user_id
|
||||
self._assertTokenRevoked(token_data_u1)
|
||||
token_data_u2 = _sample_blank_token()
|
||||
token_data_u2[field_name] = _new_id()
|
||||
self._assertTokenNotRevoked(token_data_u2)
|
||||
self.tree.remove_event(event)
|
||||
self.events.remove(event)
|
||||
self._assertTokenNotRevoked(token_data_u1)
|
||||
|
||||
def test_revoke_by_user(self):
|
||||
self._user_field_test('user_id')
|
||||
|
||||
def test_revoke_by_user_matches_trustee(self):
|
||||
self._user_field_test('trustee_id')
|
||||
|
||||
def test_revoke_by_user_matches_trustor(self):
|
||||
self._user_field_test('trustor_id')
|
||||
|
||||
def test_by_user_expiration(self):
|
||||
future_time = _future_time()
|
||||
|
||||
user_id = 1
|
||||
event = self._revoke_by_expiration(user_id, future_time)
|
||||
token_data_1 = _sample_blank_token()
|
||||
token_data_1['user_id'] = user_id
|
||||
token_data_1['expires_at'] = future_time
|
||||
self._assertTokenRevoked(token_data_1)
|
||||
|
||||
token_data_2 = _sample_blank_token()
|
||||
token_data_2['user_id'] = user_id
|
||||
expire_delta = datetime.timedelta(seconds=2000)
|
||||
future_time = timeutils.utcnow() + expire_delta
|
||||
token_data_2['expires_at'] = future_time
|
||||
self._assertTokenNotRevoked(token_data_2)
|
||||
|
||||
self.removeEvent(event)
|
||||
self._assertTokenNotRevoked(token_data_1)
|
||||
|
||||
def removeEvent(self, event):
|
||||
self.events.remove(event)
|
||||
self.tree.remove_event(event)
|
||||
|
||||
def test_by_project_grant(self):
|
||||
token_to_revoke = self.token_to_revoke
|
||||
tokens = self.project_tokens
|
||||
|
||||
self._assertTokenNotRevoked(token_to_revoke)
|
||||
for token in tokens:
|
||||
self._assertTokenNotRevoked(token)
|
||||
|
||||
event = self._revoke_by_grant(role_id=self.role_ids[0],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
|
||||
self._assertTokenRevoked(token_to_revoke)
|
||||
for token in tokens:
|
||||
self._assertTokenNotRevoked(token)
|
||||
|
||||
self.removeEvent(event)
|
||||
|
||||
self._assertTokenNotRevoked(token_to_revoke)
|
||||
for token in tokens:
|
||||
self._assertTokenNotRevoked(token)
|
||||
|
||||
token_to_revoke['roles'] = [self.role_ids[0],
|
||||
self.role_ids[1],
|
||||
self.role_ids[2]]
|
||||
|
||||
event = self._revoke_by_grant(role_id=self.role_ids[0],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
self._assertTokenRevoked(token_to_revoke)
|
||||
self.removeEvent(event)
|
||||
self._assertTokenNotRevoked(token_to_revoke)
|
||||
|
||||
event = self._revoke_by_grant(role_id=self.role_ids[1],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
self._assertTokenRevoked(token_to_revoke)
|
||||
self.removeEvent(event)
|
||||
self._assertTokenNotRevoked(token_to_revoke)
|
||||
|
||||
self._revoke_by_grant(role_id=self.role_ids[0],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
self._revoke_by_grant(role_id=self.role_ids[1],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
self._revoke_by_grant(role_id=self.role_ids[2],
|
||||
user_id=self.user_ids[0],
|
||||
project_id=self.project_ids[0])
|
||||
self._assertTokenRevoked(token_to_revoke)
|
||||
|
||||
def _assertEmpty(self, collection):
|
||||
return self.assertEqual(0, len(collection), "collection not empty")
|
||||
|
||||
def _assertEventsMatchIteration(self, turn):
|
||||
self.assertEqual(1, len(self.tree.revoke_map))
|
||||
self.assertEqual(turn + 1, len(self.tree.revoke_map
|
||||
['trust_id=*']
|
||||
['consumer_id=*']
|
||||
['access_token_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=*']
|
||||
['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=*']
|
||||
['expires_at=*']
|
||||
['domain_id=*']))
|
||||
# 10 users added
|
||||
self.assertEqual(turn, len(self.tree.revoke_map
|
||||
['trust_id=*']
|
||||
['consumer_id=*']
|
||||
['access_token_id=*']
|
||||
['expires_at=*']
|
||||
['domain_id=*']
|
||||
['project_id=*']))
|
||||
|
||||
def test_cleanup(self):
|
||||
events = self.events
|
||||
self._assertEmpty(self.tree.revoke_map)
|
||||
for i in range(0, 10):
|
||||
events.append(
|
||||
self._revoke_by_user(_new_id()))
|
||||
events.append(
|
||||
self._revoke_by_expiration(_new_id(), _future_time()))
|
||||
events.append(
|
||||
self._revoke_by_project_role_assignment(_new_id(), _new_id()))
|
||||
events.append(
|
||||
self._revoke_by_domain_role_assignment(_new_id(), _new_id()))
|
||||
events.append(
|
||||
self._revoke_by_domain_role_assignment(_new_id(), _new_id()))
|
||||
events.append(
|
||||
self._revoke_by_user_and_project(_new_id(), _new_id()))
|
||||
self._assertEventsMatchIteration(i + 1)
|
||||
|
||||
for event in self.events:
|
||||
self.tree.remove_event(event)
|
||||
self._assertEmpty(self.tree.revoke_map)
|
|
@ -0,0 +1,6 @@
|
|||
[token]
|
||||
provider = keystone.token.providers.pki.Provider
|
||||
revoke_by_id = False
|
||||
|
||||
[revoke]
|
||||
driver = keystone.contrib.revoke.backends.kvs.Revoke
|
|
@ -0,0 +1,6 @@
|
|||
[token]
|
||||
provider = keystone.token.providers.pki.Provider
|
||||
revoke_by_id = False
|
||||
|
||||
[revoke]
|
||||
driver = keystone.contrib.revoke.backends.sql.Revoke
|
|
@ -36,6 +36,7 @@ from keystone.contrib import endpoint_filter
|
|||
from keystone.contrib import example
|
||||
from keystone.contrib import federation
|
||||
from keystone.contrib import oauth1
|
||||
from keystone.contrib import revoke
|
||||
from keystone.tests import test_sql_upgrade
|
||||
|
||||
|
||||
|
@ -180,3 +181,27 @@ class FederationExtension(test_sql_upgrade.SqlMigrateBase):
|
|||
self.assertTableDoesNotExist(self.identity_provider)
|
||||
self.assertTableDoesNotExist(self.federation_protocol)
|
||||
self.assertTableDoesNotExist(self.mapping)
|
||||
|
||||
|
||||
_REVOKE_COLUMN_NAMES = ['id', 'domain_id', 'project_id', 'user_id', 'role_id',
|
||||
'trust_id', 'consumer_id', 'access_token_id',
|
||||
'issued_before', 'expires_at', 'revoked_at']
|
||||
|
||||
|
||||
class RevokeExtension(test_sql_upgrade.SqlMigrateBase):
|
||||
|
||||
def repo_package(self):
|
||||
return revoke
|
||||
|
||||
def test_upgrade(self):
|
||||
self.assertTableDoesNotExist('revocation_event')
|
||||
self.upgrade(1, repository=self.repo_path)
|
||||
self.assertTableColumns('revocation_event',
|
||||
_REVOKE_COLUMN_NAMES)
|
||||
|
||||
def test_downgrade(self):
|
||||
self.upgrade(1, repository=self.repo_path)
|
||||
self.assertTableColumns('revocation_event',
|
||||
_REVOKE_COLUMN_NAMES)
|
||||
self.downgrade(0, repository=self.repo_path)
|
||||
self.assertTableDoesNotExist('revocation_event')
|
||||
|
|
|
@ -678,6 +678,7 @@ SAMPLE_V2_TOKEN_EXPIRED = {
|
|||
def create_v3_token():
|
||||
return {
|
||||
"token": {
|
||||
'methods': [],
|
||||
"expires_at": timeutils.isotime(CURRENT_DATE + FUTURE_DELTA),
|
||||
"issued_at": "2013-05-21T00:02:43.941473Z",
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
@ -20,6 +21,7 @@ from keystoneclient.common import cms
|
|||
from keystone import auth
|
||||
from keystone import config
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import timeutils
|
||||
from keystone import tests
|
||||
from keystone.tests import test_v3
|
||||
|
||||
|
@ -28,7 +30,7 @@ CONF = config.CONF
|
|||
|
||||
|
||||
class TestAuthInfo(test_v3.RestfulTestCase):
|
||||
# TDOD(henry-nash) These tests are somewhat inefficient, since by
|
||||
# TODO(henry-nash) These tests are somewhat inefficient, since by
|
||||
# using the test_v3.RestfulTestCase class to gain access to the auth
|
||||
# building helper functions, they cause backend databases and fixtures
|
||||
# to be loaded unnecessarily. Separating out the helper functions from
|
||||
|
@ -93,14 +95,13 @@ class TestAuthInfo(test_v3.RestfulTestCase):
|
|||
method_name)
|
||||
|
||||
|
||||
class TestPKITokenAPIs(test_v3.RestfulTestCase):
|
||||
def config_files(self):
|
||||
conf_files = super(TestPKITokenAPIs, self).config_files()
|
||||
conf_files.append(tests.dirs.tests('test_pki_token_provider.conf'))
|
||||
return conf_files
|
||||
|
||||
def setUp(self):
|
||||
super(TestPKITokenAPIs, self).setUp()
|
||||
class TokenAPITests(object):
|
||||
# Why is this not just setUP? Because TokenAPITests is not a test class
|
||||
# itself. If TokenAPITests became a subclass of the testcase, it would get
|
||||
# called by the enumerate-tests-in-file code. The way the functions get
|
||||
# resolved in Python for multiple inheritance means that a setUp in this
|
||||
# would get skipped by the testrunner.
|
||||
def doSetUp(self):
|
||||
auth_data = self.build_authentication_request(
|
||||
username=self.user['name'],
|
||||
user_domain_id=self.domain_id,
|
||||
|
@ -376,21 +377,28 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase):
|
|||
r = self.get('/auth/tokens?nocatalog', headers=headers)
|
||||
self.assertValidProjectScopedTokenResponse(r, require_catalog=False)
|
||||
|
||||
def test_revoke_token(self):
|
||||
headers = {'X-Subject-Token': self.get_scoped_token()}
|
||||
self.delete('/auth/tokens', headers=headers, expected_status=204)
|
||||
self.head('/auth/tokens', headers=headers, expected_status=404)
|
||||
# make sure we have a CRL
|
||||
r = self.get('/auth/tokens/OS-PKI/revoked')
|
||||
self.assertIn('signed', r.result)
|
||||
|
||||
class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests):
|
||||
def config_files(self):
|
||||
conf_files = super(TestPKITokenAPIs, self).config_files()
|
||||
conf_files.append(tests.dirs.tests('test_pki_token_provider.conf'))
|
||||
return conf_files
|
||||
|
||||
def setUp(self):
|
||||
super(TestPKITokenAPIs, self).setUp()
|
||||
self.doSetUp()
|
||||
|
||||
|
||||
class TestUUIDTokenAPIs(TestPKITokenAPIs):
|
||||
class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests):
|
||||
def config_files(self):
|
||||
conf_files = super(TestUUIDTokenAPIs, self).config_files()
|
||||
conf_files.append(tests.dirs.tests('test_uuid_token_provider.conf'))
|
||||
return conf_files
|
||||
|
||||
def setUp(self):
|
||||
super(TestUUIDTokenAPIs, self).setUp()
|
||||
self.doSetUp()
|
||||
|
||||
def test_v3_token_id(self):
|
||||
auth_data = self.build_authentication_request(
|
||||
user_id=self.user['id'],
|
||||
|
@ -553,9 +561,15 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase):
|
|||
token=adminB_token)
|
||||
|
||||
|
||||
class TestTokenRevoking(test_v3.RestfulTestCase):
|
||||
class TestTokenRevokeById(test_v3.RestfulTestCase):
|
||||
"""Test token revocation on the v3 Identity API."""
|
||||
|
||||
def config_files(self):
|
||||
conf_files = super(TestTokenRevokeById, self).config_files()
|
||||
conf_files.append(tests.dirs.tests(
|
||||
'test_revoke_kvs.conf'))
|
||||
return conf_files
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for Token Revoking Test Cases.
|
||||
|
||||
|
@ -579,7 +593,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||
- User1 has role2 assigned to domainA
|
||||
|
||||
"""
|
||||
super(TestTokenRevoking, self).setUp()
|
||||
super(TestTokenRevokeById, self).setUp()
|
||||
|
||||
# Start by creating a couple of domains and projects
|
||||
self.domainA = self.new_domain_ref()
|
||||
|
@ -721,46 +735,16 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||
headers={'X-Subject-Token': token},
|
||||
expected_status=404)
|
||||
|
||||
def test_deleting_role_revokes_token(self):
|
||||
"""Test deleting a role revokes token.
|
||||
|
||||
Test Plan:
|
||||
|
||||
- Add some additional test data, namely:
|
||||
- A third project (project C)
|
||||
- Three additional users - user4 owned by domainB and user5 and 6
|
||||
owned by domainA (different domain ownership should not affect
|
||||
the test results, just provided to broaden test coverage)
|
||||
- User5 is a member of group1
|
||||
- Group1 gets an additional assignment - role1 on projectB as
|
||||
well as its existing role1 on projectA
|
||||
- User4 has role2 on Project C
|
||||
- User6 has role1 on projectA and domainA
|
||||
- This allows us to create 5 tokens by virtue of different types of
|
||||
role assignment:
|
||||
- user1, scoped to ProjectA by virtue of user role1 assignment
|
||||
- user5, scoped to ProjectB by virtue of group role1 assignment
|
||||
- user4, scoped to ProjectC by virtue of user role2 assignment
|
||||
- user6, scoped to ProjectA by virtue of user role1 assignment
|
||||
- user6, scoped to DomainA by virtue of user role1 assignment
|
||||
- role1 is then deleted
|
||||
- Check the tokens on Project A and B, and DomainA are revoked,
|
||||
but not the one for Project C
|
||||
|
||||
"""
|
||||
# Add the additional test data
|
||||
def role_data_fixtures(self):
|
||||
self.projectC = self.new_project_ref(domain_id=self.domainA['id'])
|
||||
self.assignment_api.create_project(self.projectC['id'], self.projectC)
|
||||
self.user4 = self.new_user_ref(
|
||||
domain_id=self.domainB['id'])
|
||||
self.user4 = self.new_user_ref(domain_id=self.domainB['id'])
|
||||
self.user4['password'] = uuid.uuid4().hex
|
||||
self.identity_api.create_user(self.user4['id'], self.user4)
|
||||
|
||||
self.user5 = self.new_user_ref(
|
||||
domain_id=self.domainA['id'])
|
||||
self.user5['password'] = uuid.uuid4().hex
|
||||
self.identity_api.create_user(self.user5['id'], self.user5)
|
||||
|
||||
self.user6 = self.new_user_ref(
|
||||
domain_id=self.domainA['id'])
|
||||
self.user6['password'] = uuid.uuid4().hex
|
||||
|
@ -780,6 +764,34 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||
user_id=self.user6['id'],
|
||||
domain_id=self.domainA['id'])
|
||||
|
||||
def test_deleting_role_revokes_token(self):
|
||||
"""Test deleting a role revokes token.
|
||||
|
||||
Add some additional test data, namely:
|
||||
- A third project (project C)
|
||||
- Three additional users - user4 owned by domainB and user5 and 6
|
||||
owned by domainA (different domain ownership should not affect
|
||||
the test results, just provided to broaden test coverage)
|
||||
- User5 is a member of group1
|
||||
- Group1 gets an additional assignment - role1 on projectB as
|
||||
well as its existing role1 on projectA
|
||||
- User4 has role2 on Project C
|
||||
- User6 has role1 on projectA and domainA
|
||||
- This allows us to create 5 tokens by virtue of different types
|
||||
of role assignment:
|
||||
- user1, scoped to ProjectA by virtue of user role1 assignment
|
||||
- user5, scoped to ProjectB by virtue of group role1 assignment
|
||||
- user4, scoped to ProjectC by virtue of user role2 assignment
|
||||
- user6, scoped to ProjectA by virtue of user role1 assignment
|
||||
- user6, scoped to DomainA by virtue of user role1 assignment
|
||||
- role1 is then deleted
|
||||
- Check the tokens on Project A and B, and DomainA are revoked,
|
||||
but not the one for Project C
|
||||
|
||||
"""
|
||||
|
||||
self.role_data_fixtures()
|
||||
|
||||
# Now we are ready to start issuing requests
|
||||
auth_data = self.build_authentication_request(
|
||||
user_id=self.user1['id'],
|
||||
|
@ -1084,13 +1096,13 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||
self.head('/auth/tokens',
|
||||
headers={'X-Subject-Token': token2},
|
||||
expected_status=204)
|
||||
# Adding user2 to a group should invalidate token
|
||||
# Adding user2 to a group should not invalidate token
|
||||
self.put('/groups/%(group_id)s/users/%(user_id)s' % {
|
||||
'group_id': self.group2['id'],
|
||||
'user_id': self.user2['id']})
|
||||
self.head('/auth/tokens',
|
||||
headers={'X-Subject-Token': token2},
|
||||
expected_status=404)
|
||||
expected_status=204)
|
||||
|
||||
def test_removing_role_assignment_does_not_affect_other_users(self):
|
||||
"""Revoking a role from one user should not affect other users."""
|
||||
|
@ -1164,6 +1176,195 @@ class TestTokenRevoking(test_v3.RestfulTestCase):
|
|||
self.head(role_path, expected_status=404)
|
||||
|
||||
|
||||
class TestTokenRevokeApi(TestTokenRevokeById):
|
||||
EXTENSION_NAME = 'revoke'
|
||||
EXTENSION_TO_ADD = 'revoke_extension'
|
||||
|
||||
"""Test token revocation on the v3 Identity API."""
|
||||
def config_files(self):
|
||||
conf_files = super(TestTokenRevokeApi, self).config_files()
|
||||
conf_files.append(tests.dirs.tests(
|
||||
'test_revoke_kvs.conf'))
|
||||
return conf_files
|
||||
|
||||
def assertValidDeletedProjectResponse(self, events_response, project_id):
|
||||
events = events_response['events']
|
||||
self.assertEqual(1, len(events))
|
||||
self.assertEqual(project_id, events[0]['project_id'])
|
||||
self.assertIsNotNone(events[0]['issued_before'])
|
||||
self.assertIsNotNone(events_response['links'])
|
||||
del (events_response['events'][0]['issued_before'])
|
||||
del (events_response['links'])
|
||||
expected_response = {'events': [{'project_id': project_id}]}
|
||||
self.assertEqual(expected_response, events_response)
|
||||
|
||||
def assertDomainInList(self, events_response, domain_id):
|
||||
events = events_response['events']
|
||||
self.assertEqual(1, len(events))
|
||||
self.assertEqual(domain_id, events[0]['domain_id'])
|
||||
self.assertIsNotNone(events[0]['issued_before'])
|
||||
self.assertIsNotNone(events_response['links'])
|
||||
del (events_response['events'][0]['issued_before'])
|
||||
del (events_response['links'])
|
||||
expected_response = {'events': [{'domain_id': domain_id}]}
|
||||
self.assertEqual(expected_response, events_response)
|
||||
|
||||
def assertValidRevokedTokenResponse(self, events_response, user_id):
|
||||
events = events_response['events']
|
||||
self.assertEqual(1, len(events))
|
||||
self.assertEqual(user_id, events[0]['user_id'])
|
||||
self.assertIsNotNone(events[0]['expires_at'])
|
||||
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_response = {'events': [{'user_id': user_id}]}
|
||||
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=204)
|
||||
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'])
|
||||
|
||||
def get_v2_token(self):
|
||||
body = {
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'username': self.default_domain_user['name'],
|
||||
'password': self.default_domain_user['password'],
|
||||
},
|
||||
},
|
||||
}
|
||||
r = self.admin_request(method='POST', path='/v2.0/tokens', body=body)
|
||||
return r.json_body['access']['token']['id']
|
||||
|
||||
def test_revoke_v2_token(self):
|
||||
token = self.get_v2_token()
|
||||
headers = {'X-Subject-Token': token}
|
||||
self.head('/auth/tokens', headers=headers, expected_status=204)
|
||||
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'])
|
||||
|
||||
def test_revoke_by_id_false_410(self):
|
||||
self.get('/auth/tokens/OS-PKI/revoked', expected_status=410)
|
||||
|
||||
def test_list_delete_project_shows_in_event_list(self):
|
||||
self.role_data_fixtures()
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body['events']
|
||||
self.assertEqual([], events)
|
||||
self.delete(
|
||||
'/projects/%(project_id)s' % {'project_id': self.projectA['id']})
|
||||
events_response = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body
|
||||
|
||||
self.assertValidDeletedProjectResponse(events_response,
|
||||
self.projectA['id'])
|
||||
|
||||
def test_disable_domain_shows_in_event_list(self):
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body['events']
|
||||
self.assertEqual([], events)
|
||||
disable_body = {'domain': {'enabled': False}}
|
||||
self.patch(
|
||||
'/domains/%(project_id)s' % {'project_id': self.domainA['id']},
|
||||
body=disable_body)
|
||||
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body
|
||||
|
||||
self.assertDomainInList(events, self.domainA['id'])
|
||||
|
||||
def assertUserAndExpiryInList(self, events, user_id, expires_at):
|
||||
found = False
|
||||
for e in events:
|
||||
if e['user_id'] == user_id and e['expires_at'] == expires_at:
|
||||
found = True
|
||||
self.assertTrue(found,
|
||||
'event with correct user_id %s and expires_at value '
|
||||
'not in list' % user_id)
|
||||
|
||||
def test_list_delete_token_shows_in_event_list(self):
|
||||
self.role_data_fixtures()
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body['events']
|
||||
self.assertEqual([], events)
|
||||
|
||||
scoped_token = self.get_scoped_token()
|
||||
headers = {'X-Subject-Token': scoped_token}
|
||||
auth_req = self.build_authentication_request(token=scoped_token)
|
||||
response = self.post('/auth/tokens', body=auth_req)
|
||||
token2 = response.json_body['token']
|
||||
headers2 = {'X-Subject-Token': response.headers['X-Subject-Token']}
|
||||
|
||||
response = self.post('/auth/tokens', body=auth_req)
|
||||
response.json_body['token']
|
||||
headers3 = {'X-Subject-Token': response.headers['X-Subject-Token']}
|
||||
|
||||
scoped_token = self.get_scoped_token()
|
||||
headers_unrevoked = {'X-Subject-Token': scoped_token}
|
||||
|
||||
self.head('/auth/tokens', headers=headers, expected_status=204)
|
||||
self.head('/auth/tokens', headers=headers2, expected_status=204)
|
||||
self.head('/auth/tokens', headers=headers3, expected_status=204)
|
||||
self.head('/auth/tokens', headers=headers_unrevoked,
|
||||
expected_status=204)
|
||||
|
||||
self.delete('/auth/tokens', headers=headers, expected_status=204)
|
||||
# NOTE(ayoung): not deleting token3, as it should be deleted
|
||||
# by previous
|
||||
events_response = self.get('/OS-REVOKE/events',
|
||||
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'])
|
||||
self.head('/auth/tokens', headers=headers, expected_status=404)
|
||||
self.head('/auth/tokens', headers=headers2, expected_status=404)
|
||||
self.head('/auth/tokens', headers=headers3, expected_status=404)
|
||||
self.head('/auth/tokens', headers=headers_unrevoked,
|
||||
expected_status=204)
|
||||
|
||||
def test_list_with_filter(self):
|
||||
|
||||
self.role_data_fixtures()
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body['events']
|
||||
self.assertEqual(0, len(events))
|
||||
|
||||
scoped_token = self.get_scoped_token()
|
||||
headers = {'X-Subject-Token': scoped_token}
|
||||
auth = self.build_authentication_request(token=scoped_token)
|
||||
response = self.post('/auth/tokens', body=auth)
|
||||
headers2 = {'X-Subject-Token': response.headers['X-Subject-Token']}
|
||||
self.delete('/auth/tokens', headers=headers, expected_status=204)
|
||||
self.delete('/auth/tokens', headers=headers2, expected_status=204)
|
||||
|
||||
events = self.get('/OS-REVOKE/events',
|
||||
expected_status=200).json_body['events']
|
||||
|
||||
self.assertEqual(2, len(events))
|
||||
future = timeutils.isotime(timeutils.utcnow() +
|
||||
datetime.timedelta(seconds=1000))
|
||||
|
||||
events = self.get('/OS-REVOKE/events?since=%s' % (future),
|
||||
expected_status=200).json_body['events']
|
||||
self.assertEqual(0, len(events))
|
||||
|
||||
|
||||
class TestAuthExternalDisabled(test_v3.RestfulTestCase):
|
||||
def config_files(self):
|
||||
cfg_list = self._config_file_list[:]
|
||||
|
@ -1233,7 +1434,7 @@ class TestAuthExternalLegacyDomain(test_v3.RestfulTestCase):
|
|||
# '@' character.
|
||||
user = {'name': 'myname@mydivision'}
|
||||
self.identity_api.update_user(self.user['id'], user)
|
||||
remote_user = '%s@%s' % (user["name"], self.domain['name'])
|
||||
remote_user = '%s@%s' % (user['name'], self.domain['name'])
|
||||
context, auth_info, auth_context = self.build_external_auth_request(
|
||||
remote_user)
|
||||
|
||||
|
@ -1284,7 +1485,7 @@ class TestAuthExternalDomain(test_v3.RestfulTestCase):
|
|||
# '@' character.
|
||||
user = {'name': 'myname@mydivision'}
|
||||
self.identity_api.update_user(self.user['id'], user)
|
||||
remote_user = user["name"]
|
||||
remote_user = user['name']
|
||||
context, auth_info, auth_context = self.build_external_auth_request(
|
||||
remote_user, remote_domain=remote_domain)
|
||||
|
||||
|
@ -2015,6 +2216,15 @@ class TestTrustOptional(test_v3.RestfulTestCase):
|
|||
|
||||
|
||||
class TestTrustAuth(TestAuthInfo):
|
||||
EXTENSION_NAME = 'revoke'
|
||||
EXTENSION_TO_ADD = 'revoke_extension'
|
||||
|
||||
def config_files(self):
|
||||
conf_files = super(TestTrustAuth, self).config_files()
|
||||
conf_files.append(tests.dirs.tests(
|
||||
'test_revoke_kvs.conf'))
|
||||
return conf_files
|
||||
|
||||
def setUp(self):
|
||||
self.opt_in_group('trust', enabled=True)
|
||||
super(TestTrustAuth, self).setUp()
|
||||
|
@ -2456,6 +2666,44 @@ class TestTrustAuth(TestAuthInfo):
|
|||
self.assertEqual(r.result['token']['project']['name'],
|
||||
self.project['name'])
|
||||
|
||||
def assertTrustTokensRevoked(self, trust_id):
|
||||
revocation_response = self.get('/OS-REVOKE/events',
|
||||
expected_status=200)
|
||||
revocation_events = revocation_response.json_body['events']
|
||||
found = False
|
||||
for event in revocation_events:
|
||||
if event.get('OS-TRUST:trust_id') == trust_id:
|
||||
found = True
|
||||
self.assertTrue(found, 'event with trust_id %s not found in list' %
|
||||
trust_id)
|
||||
|
||||
def test_delete_trust_revokes_tokens(self):
|
||||
ref = self.new_trust_ref(
|
||||
trustor_user_id=self.user_id,
|
||||
trustee_user_id=self.trustee_user_id,
|
||||
project_id=self.project_id,
|
||||
impersonation=False,
|
||||
expires=dict(minutes=1),
|
||||
role_ids=[self.role_id])
|
||||
del ref['id']
|
||||
r = self.post('/OS-TRUST/trusts', body={'trust': ref})
|
||||
trust = self.assertValidTrustResponse(r)
|
||||
trust_id = trust['id']
|
||||
auth_data = self.build_authentication_request(
|
||||
user_id=self.trustee_user['id'],
|
||||
password=self.trustee_user['password'],
|
||||
trust_id=trust_id)
|
||||
r = self.post('/auth/tokens', body=auth_data)
|
||||
self.assertValidProjectTrustScopedTokenResponse(
|
||||
r, self.trustee_user)
|
||||
trust_token = r.headers['X-Subject-Token']
|
||||
self.delete('/OS-TRUST/trusts/%(trust_id)s' % {
|
||||
'trust_id': trust_id},
|
||||
expected_status=204)
|
||||
headers = {'X-Subject-Token': trust_token}
|
||||
self.head('/auth/tokens', headers=headers, expected_status=404)
|
||||
self.assertTrustTokensRevoked(trust_id)
|
||||
|
||||
def test_delete_trust(self):
|
||||
ref = self.new_trust_ref(
|
||||
trustor_user_id=self.user_id,
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
# 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
|
||||
import uuid
|
||||
|
||||
from keystone.common import dependency
|
||||
from keystone.contrib.revoke import model
|
||||
from keystone.openstack.common import timeutils
|
||||
from keystone.tests import test_v3
|
||||
from keystone import token
|
||||
|
||||
|
||||
def _future_time_string():
|
||||
expire_delta = datetime.timedelta(seconds=1000)
|
||||
future_time = timeutils.utcnow() + expire_delta
|
||||
return timeutils.isotime(future_time)
|
||||
|
||||
|
||||
@dependency.requires('revoke_api')
|
||||
class OSRevokeTests(test_v3.RestfulTestCase):
|
||||
EXTENSION_NAME = 'revoke'
|
||||
EXTENSION_TO_ADD = 'revoke_extension'
|
||||
|
||||
def test_get_empty_list(self):
|
||||
resp = self.get('/OS-REVOKE/events')
|
||||
self.assertEqual([], resp.json_body['events'])
|
||||
|
||||
def _blank_event(self):
|
||||
return {}
|
||||
|
||||
# The two values will be the same with the exception of
|
||||
# 'issued_before' which is set when the event is recorded.
|
||||
def assertReporteEventMatchesRecorded(self, event, sample, before_time):
|
||||
after_time = timeutils.utcnow()
|
||||
event_issued_before = timeutils.normalize_time(
|
||||
timeutils.parse_isotime(event['issued_before']))
|
||||
self.assertTrue(before_time < event_issued_before,
|
||||
'invalid event issued_before time; Too early')
|
||||
self.assertTrue(event_issued_before < after_time,
|
||||
'invalid event issued_before time; too late')
|
||||
del (event['issued_before'])
|
||||
self.assertEqual(sample, event)
|
||||
|
||||
def test_revoked_token_in_list(self):
|
||||
user_id = uuid.uuid4().hex
|
||||
expires_at = token.default_expire_time()
|
||||
sample = self._blank_event()
|
||||
sample['user_id'] = unicode(user_id)
|
||||
sample['expires_at'] = unicode(timeutils.isotime(expires_at,
|
||||
subsecond=True))
|
||||
before_time = timeutils.utcnow()
|
||||
self.revoke_api.revoke_by_expiration(user_id, expires_at)
|
||||
resp = self.get('/OS-REVOKE/events')
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertReporteEventMatchesRecorded(events[0], sample, before_time)
|
||||
|
||||
def test_disabled_project_in_list(self):
|
||||
project_id = uuid.uuid4().hex
|
||||
sample = dict()
|
||||
sample['project_id'] = unicode(project_id)
|
||||
before_time = timeutils.utcnow()
|
||||
self.revoke_api.revoke(
|
||||
model.RevokeEvent(project_id=project_id))
|
||||
|
||||
resp = self.get('/OS-REVOKE/events')
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertReporteEventMatchesRecorded(events[0], sample, before_time)
|
||||
|
||||
def test_disabled_domain_in_list(self):
|
||||
domain_id = uuid.uuid4().hex
|
||||
sample = dict()
|
||||
sample['domain_id'] = unicode(domain_id)
|
||||
before_time = timeutils.utcnow()
|
||||
self.revoke_api.revoke(
|
||||
model.RevokeEvent(domain_id=domain_id))
|
||||
|
||||
resp = self.get('/OS-REVOKE/events')
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertReporteEventMatchesRecorded(events[0], sample, before_time)
|
||||
|
||||
def test_list_since_invalid(self):
|
||||
self.get('/OS-REVOKE/events?since=blah', expected_status=400)
|
||||
|
||||
def test_list_since_valid(self):
|
||||
resp = self.get('/OS-REVOKE/events?since=2013-02-27T18:30:59.999999Z')
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual(len(events), 0)
|
||||
|
||||
def test_since_future_time_no_events(self):
|
||||
domain_id = uuid.uuid4().hex
|
||||
sample = dict()
|
||||
sample['domain_id'] = unicode(domain_id)
|
||||
|
||||
self.revoke_api.revoke(
|
||||
model.RevokeEvent(domain_id=domain_id))
|
||||
|
||||
resp = self.get('/OS-REVOKE/events')
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual(len(events), 1)
|
||||
|
||||
resp = self.get('/OS-REVOKE/events?since=%s' % _future_time_string())
|
||||
events = resp.json_body['events']
|
||||
self.assertEqual([], events)
|
|
@ -299,6 +299,15 @@ class Token(token.Driver):
|
|||
|
||||
def _list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
# This function is used to generate the list of tokens that should be
|
||||
# revoked when revoking by token identifiers. This approach will be
|
||||
# deprecated soon, probably in the Juno release. Setting revoke_by_id
|
||||
# to False indicates that this kind of recording should not be
|
||||
# performed. In order to test the revocation events, tokens shouldn't
|
||||
# be deleted from the backends. This check ensures that tokens are
|
||||
# still recorded.
|
||||
if not CONF.token.revoke_by_id:
|
||||
return []
|
||||
tokens = []
|
||||
user_key = self._prefix_user_id(user_id)
|
||||
token_list = self._get_user_token_list_with_expiry(user_key)
|
||||
|
|
|
@ -15,12 +15,16 @@
|
|||
import copy
|
||||
|
||||
from keystone.common import sql
|
||||
from keystone import config
|
||||
from keystone import exception
|
||||
from keystone.openstack.common.db.sqlalchemy import session as db_session
|
||||
from keystone.openstack.common import timeutils
|
||||
from keystone import token
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class TokenModel(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'token'
|
||||
attributes = ['id', 'expires', 'user_id', 'trust_id']
|
||||
|
@ -164,6 +168,8 @@ class Token(token.Driver):
|
|||
|
||||
def _list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
if not CONF.token.revoke_by_id:
|
||||
return []
|
||||
if trust_id:
|
||||
return self._list_tokens_for_trust(trust_id)
|
||||
if consumer_id:
|
||||
|
|
|
@ -391,6 +391,7 @@ class Auth(controller.V2Controller):
|
|||
Identical to ``validate_token``, except does not return a response.
|
||||
|
||||
"""
|
||||
# TODO(ayoung) validate against revocation API
|
||||
belongs_to = context['query_string'].get('belongsTo')
|
||||
self.token_provider_api.check_v2_token(token_id, belongs_to)
|
||||
|
||||
|
@ -405,6 +406,7 @@ class Auth(controller.V2Controller):
|
|||
|
||||
"""
|
||||
belongs_to = context['query_string'].get('belongsTo')
|
||||
# TODO(ayoung) validate against revocation API
|
||||
return self.token_provider_api.validate_v2_token(token_id, belongs_to)
|
||||
|
||||
@controller.v2_deprecated
|
||||
|
@ -412,11 +414,13 @@ class Auth(controller.V2Controller):
|
|||
"""Delete a token, effectively invalidating it for authz."""
|
||||
# TODO(termie): this stuff should probably be moved to middleware
|
||||
self.assert_admin(context)
|
||||
self.token_api.delete_token(token_id)
|
||||
self.token_provider_api.revoke_token(token_id)
|
||||
|
||||
@controller.v2_deprecated
|
||||
@controller.protected()
|
||||
def revocation_list(self, context, auth=None):
|
||||
if not CONF.token.revoke_by_id:
|
||||
raise exception.Gone()
|
||||
tokens = self.token_api.list_revoked_tokens()
|
||||
|
||||
for t in tokens:
|
||||
|
|
|
@ -164,6 +164,8 @@ class Manager(manager.Manager):
|
|||
return ret
|
||||
|
||||
def delete_token(self, token_id):
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
unique_id = self.unique_id(token_id)
|
||||
self.driver.delete_token(unique_id)
|
||||
self._invalidate_individual_token_cache(unique_id)
|
||||
|
@ -171,6 +173,8 @@ class Manager(manager.Manager):
|
|||
|
||||
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
|
||||
consumer_id=None):
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
token_list = self.driver._list_tokens(user_id, tenant_id, trust_id,
|
||||
consumer_id)
|
||||
self.driver.delete_tokens(user_id, tenant_id, trust_id, consumer_id)
|
||||
|
@ -192,6 +196,8 @@ class Manager(manager.Manager):
|
|||
|
||||
def delete_tokens_for_domain(self, domain_id):
|
||||
"""Delete all tokens for a given domain."""
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
projects = self.assignment_api.list_projects()
|
||||
for project in projects:
|
||||
if project['domain_id'] == domain_id:
|
||||
|
@ -207,6 +213,8 @@ class Manager(manager.Manager):
|
|||
revocations in a single call instead of needing to explicitly handle
|
||||
trusts in the caller's logic.
|
||||
"""
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
self.delete_tokens(user_id, tenant_id=project_id)
|
||||
for trust in self.trust_api.list_trusts_for_trustee(user_id):
|
||||
# Ensure we revoke tokens associated to the trust / project
|
||||
|
@ -234,6 +242,8 @@ class Manager(manager.Manager):
|
|||
:param user_ids: list of user identifiers
|
||||
:param project_id: optional project identifier
|
||||
"""
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
for user_id in user_ids:
|
||||
self.delete_tokens_for_user(user_id, project_id=project_id)
|
||||
|
||||
|
@ -353,6 +363,8 @@ class Driver(object):
|
|||
:raises: keystone.exception.TokenNotFound
|
||||
|
||||
"""
|
||||
if not CONF.token.revoke_by_id:
|
||||
return
|
||||
token_list = self._list_tokens(user_id,
|
||||
tenant_id=tenant_id,
|
||||
trust_id=trust_id,
|
||||
|
|
|
@ -22,6 +22,8 @@ from keystone.common import cache
|
|||
from keystone.common import dependency
|
||||
from keystone.common import manager
|
||||
from keystone import config
|
||||
from keystone.contrib.revoke import model as revoke_model
|
||||
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import log
|
||||
from keystone.openstack.common import timeutils
|
||||
|
@ -50,6 +52,7 @@ class UnsupportedTokenVersionException(Exception):
|
|||
|
||||
|
||||
@dependency.requires('token_api')
|
||||
@dependency.optional('revoke_api')
|
||||
@dependency.provider('token_provider_api')
|
||||
class Manager(manager.Manager):
|
||||
"""Default pivot point for the token provider backend.
|
||||
|
@ -115,15 +118,43 @@ class Manager(manager.Manager):
|
|||
self._is_valid_token(token)
|
||||
return token
|
||||
|
||||
def check_revocation_v2(self, token):
|
||||
try:
|
||||
token_data = token['access']
|
||||
except KeyError:
|
||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
||||
|
||||
token_values = revoke_model.build_token_values_v2(
|
||||
token_data, CONF.identity.default_domain_id)
|
||||
if self.revoke_api is not None:
|
||||
self.revoke_api.check_token(token_values)
|
||||
|
||||
def validate_v2_token(self, token_id, belongs_to=None):
|
||||
unique_id = self.token_api.unique_id(token_id)
|
||||
# NOTE(morganfainberg): Ensure we never use the long-form token_id
|
||||
# (PKI) as part of the cache_key.
|
||||
token = self._validate_v2_token(unique_id)
|
||||
self.check_revocation_v2(token)
|
||||
self._token_belongs_to(token, belongs_to)
|
||||
self._is_valid_token(token)
|
||||
return token
|
||||
|
||||
def check_revocation_v3(self, token):
|
||||
try:
|
||||
token_data = token['token']
|
||||
except KeyError:
|
||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
||||
token_values = revoke_model.build_token_values(token_data)
|
||||
if self.revoke_api is not None:
|
||||
self.revoke_api.check_token(token_values)
|
||||
|
||||
def check_revocation(self, token):
|
||||
version = self.driver.get_token_version(token)
|
||||
if version == V2:
|
||||
return self.check_revocation_v2(token)
|
||||
else:
|
||||
return self.check_revocation_v3(token)
|
||||
|
||||
def validate_v3_token(self, token_id):
|
||||
unique_id = self.token_api.unique_id(token_id)
|
||||
# NOTE(morganfainberg): Ensure we never use the long-form token_id
|
||||
|
@ -187,14 +218,17 @@ class Manager(manager.Manager):
|
|||
expires_at = token_data['token']['expires']
|
||||
expiry = timeutils.normalize_time(
|
||||
timeutils.parse_isotime(expires_at))
|
||||
if current_time < expiry:
|
||||
# Token is has not expired and has not been revoked.
|
||||
return None
|
||||
except Exception:
|
||||
LOG.exception(_('Unexpected error or malformed token determining '
|
||||
'token expiry: %s'), token)
|
||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
||||
|
||||
raise exception.TokenNotFound(_("The token is malformed or expired."))
|
||||
if current_time < expiry:
|
||||
self.check_revocation(token)
|
||||
# Token has not expired and has not been revoked.
|
||||
return None
|
||||
else:
|
||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
||||
|
||||
def _token_belongs_to(self, token, belongs_to):
|
||||
"""Check if the token belongs to the right tenant.
|
||||
|
|
|
@ -356,7 +356,7 @@ class V3TokenDataHelper(object):
|
|||
|
||||
@dependency.optional('oauth_api')
|
||||
@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
|
||||
'token_api', 'trust_api')
|
||||
'revoke_api', 'token_api', 'trust_api')
|
||||
class BaseProvider(provider.Provider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BaseProvider, self).__init__(*args, **kwargs)
|
||||
|
@ -525,7 +525,19 @@ class BaseProvider(provider.Provider):
|
|||
return token_ref
|
||||
|
||||
def revoke_token(self, token_id):
|
||||
self.token_api.delete_token(token_id=token_id)
|
||||
token = self.token_api.get_token(token_id)
|
||||
if self.revoke_api:
|
||||
version = self.get_token_version(token)
|
||||
if version == provider.V3:
|
||||
user_id = token['user']['id']
|
||||
expires_at = token['expires']
|
||||
elif version == provider.V2:
|
||||
user_id = token['user_id']
|
||||
expires_at = token['expires']
|
||||
self.revoke_api.revoke_by_expiration(user_id, expires_at)
|
||||
|
||||
if CONF.token.revoke_by_id:
|
||||
self.token_api.delete_token(token_id=token_id)
|
||||
|
||||
def _assert_default_domain(self, token_ref):
|
||||
"""Make sure we are operating on default domain only."""
|
||||
|
@ -616,9 +628,8 @@ class BaseProvider(provider.Provider):
|
|||
token_ref = self._verify_token(token_id)
|
||||
token_data = self._validate_v3_token_ref(token_ref)
|
||||
return token_data
|
||||
except (exception.ValidationError,
|
||||
exception.UserNotFound):
|
||||
LOG.exception(_('Failed to validate token'))
|
||||
except (exception.ValidationError, exception.UserNotFound):
|
||||
raise exception.TokenNotFound(token_id)
|
||||
|
||||
def _validate_v3_token_ref(self, token_ref):
|
||||
# FIXME(gyee): performance or correctness? Should we return the
|
||||
|
|
Loading…
Reference in New Issue