Merge "Simplify token persistence callbacks"

This commit is contained in:
Zuul 2018-02-19 17:16:39 +00:00 committed by Gerrit Code Review
commit 7c96e99301
7 changed files with 120 additions and 148 deletions

View File

@ -57,9 +57,7 @@ class Manager(manager.Manager):
self._delete_app_creds_on_user_delete_callback)
notifications.register_event_callback(
notifications.ACTIONS.internal,
# This notification is emitted when a role assignment is removed,
# we can take advantage of it even though we're not a token.
notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
notifications.REMOVE_APP_CREDS_FOR_USER,
self._delete_app_creds_on_assignment_removal)
def _delete_app_creds_on_user_delete_callback(

View File

@ -93,6 +93,23 @@ class Manager(manager.Manager):
# Use set() to process the list to remove any duplicates
return list(set([x['user_id'] for x in assignment_list]))
def _send_app_cred_notification_for_role_removal(self, role_id):
"""Delete all application credential for a specific role.
:param role_id: role identifier
:type role_id: string
"""
assignments = self.list_role_assignments(role_id=role_id)
for assignment in assignments:
if 'user_id' in assignment and 'project_id' in assignment:
payload = {
'user_id': assignment['user_id'],
'project_id': assignment['project_id']
}
notifications.Audit.internal(
notifications.REMOVE_APP_CREDS_FOR_USER, payload
)
@MEMOIZE_COMPUTED_ASSIGNMENTS
def get_roles_for_user_and_project(self, user_id, tenant_id):
"""Get the roles associated with a user within given project.
@ -241,32 +258,45 @@ class Manager(manager.Manager):
self.driver.remove_role_from_user_and_project(user_id, project_id,
role_id)
if project_id:
self._emit_invalidate_grant_token_persistence(user_id, project_id)
else:
PROVIDERS.identity_api.emit_invalidate_user_token_persistence(
user_id)
payload = {'user_id': user_id, 'project_id': project_id}
notifications.Audit.internal(
notifications.REMOVE_APP_CREDS_FOR_USER,
payload
)
self._invalidate_token_cache(
role_id, group_id, user_id, project_id, domain_id
)
def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
self._remove_role_from_user_and_project_adapter(
role_id, user_id=user_id, project_id=tenant_id)
COMPUTED_ASSIGNMENTS_REGION.invalidate()
def _emit_invalidate_user_token_persistence(self, user_id):
PROVIDERS.identity_api.emit_invalidate_user_token_persistence(user_id)
def _invalidate_token_cache(self, role_id, group_id, user_id, project_id,
domain_id):
if group_id:
actor_type = 'group'
actor_id = group_id
elif user_id:
actor_type = 'user'
actor_id = user_id
# NOTE(lbragstad): The previous notification decorator behavior didn't
# send the notification unless the operation was successful. We
# maintain that behavior here by calling to the notification module
# after the call to emit invalid user tokens.
notifications.Audit.internal(
notifications.INVALIDATE_USER_TOKEN_PERSISTENCE, user_id
)
if domain_id:
target_type = 'domain'
target_id = domain_id
elif project_id:
target_type = 'project'
target_id = project_id
def _emit_invalidate_grant_token_persistence(self, user_id, project_id):
PROVIDERS.identity_api.emit_invalidate_grant_token_persistence(
{'user_id': user_id, 'project_id': project_id}
reason = (
'Invalidating the token cache because role %(role_id)s was '
'removed from %(actor_type)s %(actor_id)s on %(target_type)s '
'%(target_id)s.' %
{'role_id': role_id, 'actor_type': actor_type,
'actor_id': actor_id, 'target_type': target_type,
'target_id': target_id}
)
notifications.invalidate_token_cache_notification(reason)
@notifications.role_assignment('created')
def create_grant(self, role_id, user_id=None, group_id=None,
@ -318,10 +348,6 @@ class Manager(manager.Manager):
)
return PROVIDERS.role_api.list_roles_from_ids(grant_ids)
def _emit_revoke_user_grant(self, role_id, user_id, domain_id, project_id,
inherited_to_projects, context):
self._emit_invalidate_grant_token_persistence(user_id, project_id)
@notifications.role_assignment('deleted')
def delete_grant(self, role_id, user_id=None, group_id=None,
domain_id=None, project_id=None,
@ -337,9 +363,9 @@ class Manager(manager.Manager):
project_id=project_id,
inherited_to_projects=inherited_to_projects
)
self._emit_revoke_user_grant(
role_id, user_id, domain_id, project_id,
inherited_to_projects, context)
self._invalidate_token_cache(
role_id, group_id, user_id, project_id, domain_id
)
else:
try:
# check if role exists on the group before revoke
@ -349,13 +375,9 @@ class Manager(manager.Manager):
inherited_to_projects=inherited_to_projects
)
if CONF.token.revoke_by_id:
# NOTE(morganfainberg): The user ids are the important part
# for invalidating tokens below, so extract them here.
for user in PROVIDERS.identity_api.list_users_in_group(
group_id):
self._emit_revoke_user_grant(
role_id, user['id'], domain_id, project_id,
inherited_to_projects, context)
self._invalidate_token_cache(
role_id, group_id, user_id, project_id, domain_id
)
except exception.GroupNotFound:
LOG.debug('Group %s not found, no tokens to invalidate.',
group_id)
@ -1053,75 +1075,6 @@ class Manager(manager.Manager):
for assignment in system_assignments:
self.delete_system_grant_for_group(group_id, assignment['id'])
def delete_tokens_for_role_assignments(self, role_id):
assignments = self.list_role_assignments(role_id=role_id)
# Iterate over the assignments for this role and build the list of
# user or user+project IDs for the tokens we need to delete
user_ids = set()
user_and_project_ids = list()
for assignment in assignments:
# If we have a project assignment, then record both the user and
# project IDs so we can target the right token to delete. If it is
# a domain assignment, we might as well kill all the tokens for
# the user, since in the vast majority of cases all the tokens
# for a user will be within one domain anyway, so not worth
# trying to delete tokens for each project in the domain. If the
# assignment is a system assignment, invalidate all tokens from the
# cache. A future patch may optimize this to only remove specific
# system-scoped tokens from the cache.
if 'user_id' in assignment:
if 'project_id' in assignment:
user_and_project_ids.append(
(assignment['user_id'], assignment['project_id']))
elif 'domain_id' or 'system' in assignment:
self._emit_invalidate_user_token_persistence(
assignment['user_id'])
elif 'group_id' in assignment:
# Add in any users for this group, being tolerant of any
# cross-driver database integrity errors.
try:
users = PROVIDERS.identity_api.list_users_in_group(
assignment['group_id'])
except exception.GroupNotFound:
# Ignore it, but log a debug message
if 'project_id' in assignment:
target = _('Project (%s)') % assignment['project_id']
elif 'domain_id' in assignment:
target = _('Domain (%s)') % assignment['domain_id']
else:
target = _('Unknown Target')
msg = ('Group (%(group)s), referenced in assignment '
'for %(target)s, not found - ignoring.')
LOG.debug(msg, {'group': assignment['group_id'],
'target': target})
continue
if 'project_id' in assignment:
for user in users:
user_and_project_ids.append(
(user['id'], assignment['project_id']))
elif 'domain_id' or 'system' in assignment:
for user in users:
self._emit_invalidate_user_token_persistence(
user['id'])
# Now process the built up lists. Before issuing calls to delete any
# tokens, let's try and minimize the number of calls by pruning out
# any user+project deletions where a general token deletion for that
# same user is also planned.
user_and_project_ids_to_action = []
for user_and_project_id in user_and_project_ids:
if user_and_project_id[0] not in user_ids:
user_and_project_ids_to_action.append(user_and_project_id)
for user_id, project_id in user_and_project_ids_to_action:
payload = {'user_id': user_id, 'project_id': project_id}
notifications.Audit.internal(
notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
payload
)
def delete_user_assignments(self, user_id):
# FIXME(lbragstad): This should be refactored in the Rocky release so
# that we can pass the user_id to the system assignment backend like we
@ -1355,11 +1308,20 @@ class RoleManager(manager.Manager):
return ret
def delete_role(self, role_id, initiator=None):
PROVIDERS.assignment_api.delete_tokens_for_role_assignments(role_id)
PROVIDERS.assignment_api.delete_role_assignments(role_id)
PROVIDERS.assignment_api._send_app_cred_notification_for_role_removal(
role_id
)
self.driver.delete_role(role_id)
notifications.Audit.deleted(self._ROLE, role_id, initiator)
self.get_role.invalidate(self, role_id)
reason = (
'Invalidating the token cache because role %(role_id)s has been '
'removed. Role assignments for users will be recalculated and '
'enforced accordingly the next time they authenticate or validate '
'a token' % {'role_id': role_id}
)
notifications.invalidate_token_cache_notification(reason)
COMPUTED_ASSIGNMENTS_REGION.invalidate()
# TODO(ayoung): Add notification

View File

@ -1128,7 +1128,14 @@ class Manager(manager.Manager):
enabled_change = ((user.get('enabled') is False) and
user['enabled'] != old_user_ref.get('enabled'))
if enabled_change or user.get('password') is not None:
self.emit_invalidate_user_token_persistence(user_id)
self._persist_revocation_event_for_user(user_id)
reason = (
'Invalidating the token cache because user %(user_id)s was '
'enabled or disabled. Authorization will be calculated and '
'enforced accordingly the next time they authenticate or '
'validate a token.' % {'user_id': user_id}
)
notifications.invalidate_token_cache_notification(reason)
return self._set_domain_id_and_mapping(
ref, domain_id, driver, mapping.EntityType.USER)
@ -1231,8 +1238,8 @@ class Manager(manager.Manager):
# assignment for the group then we do not need to revoke all the users
# tokens and can just delete the group.
if roles:
for uid in user_ids:
self.emit_invalidate_user_token_persistence(uid)
for user_id in user_ids:
self._persist_revocation_event_for_user(user_id)
# Invalidate user role assignments cache region, as it may be caching
# role assignments expanded from the specified group to its users
@ -1281,7 +1288,7 @@ class Manager(manager.Manager):
user_entity_id, user_driver, group_entity_id, group_driver)
group_driver.remove_user_from_group(user_entity_id, group_entity_id)
self.emit_invalidate_user_token_persistence(user_id)
self._persist_revocation_event_for_user(user_id)
# Invalidate user role assignments cache region, as it may be caching
# role assignments expanded from this group to this user
@ -1289,31 +1296,17 @@ class Manager(manager.Manager):
notifications.Audit.removed_from(self._GROUP, group_id, self._USER,
user_id, initiator)
def emit_invalidate_user_token_persistence(self, user_id):
"""Emit a notification to the callback system to revoke user tokens.
def _persist_revocation_event_for_user(self, user_id):
"""Emit a notification to invoke a revocation event callback.
This method and associated callback listener removes the need for
making a direct call to another manager to delete and revoke tokens.
Fire off an internal notification that will be consumed by the
revocation API to store a revocation record for a specific user.
:param user_id: user identifier
:type user_id: string
"""
notifications.Audit.internal(
notifications.INVALIDATE_USER_TOKEN_PERSISTENCE, user_id
)
def emit_invalidate_grant_token_persistence(self, user_project):
"""Emit a notification to the callback system to revoke grant tokens.
This method and associated callback listener removes the need for
making a direct call to another manager to delete and revoke tokens.
:param user_project: {'user_id': user_id, 'project_id': project_id}
:type user_project: dict
"""
notifications.Audit.internal(
notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
user_project
notifications.PERSIST_REVOCATION_EVENT_FOR_USER, user_id
)
@domains_configured
@ -1408,7 +1401,7 @@ class Manager(manager.Manager):
raise
notifications.Audit.updated(self._USER, user_id, initiator)
self.emit_invalidate_user_token_persistence(user_id)
self._persist_revocation_event_for_user(user_id)
@MEMOIZE
def _shadow_nonlocal_user(self, user):

View File

@ -76,8 +76,9 @@ CONF = keystone.conf.CONF
# NOTE(morganfainberg): Special case notifications that are only used
# internally for handling token persistence token deletions
INVALIDATE_USER_TOKEN_PERSISTENCE = 'invalidate_user_tokens'
INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE = 'invalidate_user_project_tokens'
INVALIDATE_TOKEN_CACHE = 'invalidate_token_cache'
PERSIST_REVOCATION_EVENT_FOR_USER = 'persist_revocation_event_for_user'
REMOVE_APP_CREDS_FOR_USER = 'remove_application_credentials_for_user'
INVALIDATE_USER_OAUTH_CONSUMER_TOKENS = 'invalidate_user_consumer_tokens'
INVALIDATE_TOKEN_CACHE_DELETED_IDP = 'invalidate_token_cache_from_deleted_idp'
DOMAIN_DELETED = 'domain_deleted'
@ -180,6 +181,32 @@ class Audit(object):
public, reason)
def invalidate_token_cache_notification(reason):
"""A specific notification for invalidating the token cache.
:param reason: The specific reason why the token cache is being
invalidated.
:type reason: string
"""
# Since keystone does a lot of work in the authentication and validation
# process to make sure the authorization context for the user is
# update-to-date, invalidating the token cache is a somewhat common
# operation. It's done across various subsystems when role assignments
# change, users are disabled, identity providers deleted or disabled, etc..
# This notification is meant to make the process of invalidating the token
# cache DRY, instead of have each subsystem implement their own token cache
# invalidation strategy or callbacks.
LOG.debug(reason)
resource_id = None
initiator = None
public = False
Audit._emit(
ACTIONS.internal, INVALIDATE_TOKEN_CACHE, resource_id, initiator,
public, reason=reason
)
def _get_callback_info(callback):
"""Return list containing callback's module and name.

View File

@ -427,16 +427,6 @@ class Manager(manager.Manager):
return ret
def _pre_delete_cleanup_project(self, project_id):
project_user_ids = (
PROVIDERS.assignment_api.list_user_ids_for_project(project_id))
for user_id in project_user_ids:
payload = {'user_id': user_id, 'project_id': project_id}
notifications.Audit.internal(
notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
payload
)
def _post_delete_cleanup_project(self, project_id, project,
initiator=None):
try:
@ -501,16 +491,20 @@ class Manager(manager.Manager):
project_list = subtree_list + [project]
projects_ids = [x['id'] for x in project_list]
for prj in project_list:
self._pre_delete_cleanup_project(prj['id'])
ret = self.driver.delete_projects_from_ids(projects_ids)
for prj in project_list:
self._post_delete_cleanup_project(prj['id'], prj, initiator)
else:
self._pre_delete_cleanup_project(project_id)
ret = self.driver.delete_project(project_id)
self._post_delete_cleanup_project(project_id, project, initiator)
reason = (
'The token cache is being invalidate because project '
'%(project_id)s was deleted. Authorization will be recalculated '
'and enforced accordingly the next time users authenticate or '
'validate a token.' % {'project_id': project_id}
)
notifications.invalidate_token_cache_notification(reason)
return ret
def _filter_projects_list(self, projects_list, user_id):

View File

@ -88,7 +88,7 @@ class Manager(manager.Manager):
['user', self._user_callback]
],
notifications.ACTIONS.internal: [
[notifications.INVALIDATE_USER_TOKEN_PERSISTENCE,
[notifications.PERSIST_REVOCATION_EVENT_FOR_USER,
self._user_callback],
]
}

View File

@ -80,14 +80,12 @@ class Manager(manager.Manager):
['project', self._drop_token_cache],
],
notifications.ACTIONS.internal: [
[notifications.INVALIDATE_USER_TOKEN_PERSISTENCE,
self._drop_token_cache],
[notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
self._drop_token_cache],
[notifications.INVALIDATE_USER_OAUTH_CONSUMER_TOKENS,
self._drop_token_cache],
[notifications.INVALIDATE_TOKEN_CACHE_DELETED_IDP,
self._drop_token_cache],
[notifications.INVALIDATE_TOKEN_CACHE,
self._drop_token_cache],
]
}