From 92fd2dbfb672d7b2b1cdfd5dd5cf89f7716b3e12 Mon Sep 17 00:00:00 2001 From: asteroide Date: Tue, 1 Sep 2015 16:03:26 +0200 Subject: Update Keystone code from official Github repository with branch Master on 09/01/2015. Change-Id: I0ff6099e6e2580f87f502002a998bbfe12673498 --- keystone-moon/keystone/assignment/backends/ldap.py | 66 ++- keystone-moon/keystone/assignment/backends/sql.py | 108 +++- keystone-moon/keystone/assignment/controllers.py | 385 ++++-------- keystone-moon/keystone/assignment/core.py | 660 +++++++++++++++------ 4 files changed, 714 insertions(+), 505 deletions(-) (limited to 'keystone-moon/keystone/assignment') diff --git a/keystone-moon/keystone/assignment/backends/ldap.py b/keystone-moon/keystone/assignment/backends/ldap.py index f93e989f..4ca66c4d 100644 --- a/keystone-moon/keystone/assignment/backends/ldap.py +++ b/keystone-moon/keystone/assignment/backends/ldap.py @@ -13,10 +13,10 @@ # under the License. from __future__ import absolute_import -import ldap as ldap import ldap.filter from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from keystone import assignment from keystone.assignment.role_backends import ldap as ldap_role @@ -25,7 +25,6 @@ from keystone.common import models from keystone import exception from keystone.i18n import _ from keystone.identity.backends import ldap as ldap_identity -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -36,7 +35,7 @@ class Assignment(assignment.Driver): @versionutils.deprecated( versionutils.deprecated.KILO, remove_in=+2, - what='keystone.assignment.backends.ldap.Assignment') + what='ldap') def __init__(self): super(Assignment, self).__init__() self.LDAP_URL = CONF.ldap.url @@ -54,10 +53,10 @@ class Assignment(assignment.Driver): self.role = RoleApi(CONF, self.user) def default_role_driver(self): - return 'keystone.assignment.role_backends.ldap.Role' + return 'ldap' def default_resource_driver(self): - return 'keystone.resource.backends.ldap.Resource' + return 'ldap' def list_role_ids_for_groups_on_project( self, groups, project_id, project_domain_id, project_parents): @@ -181,7 +180,7 @@ class Assignment(assignment.Driver): self.group._id_to_dn(group_id), role_id) # Bulk actions on User From identity - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): user_dn = self.user._id_to_dn(user_id) for ref in self.role.list_global_roles_for_user(user_dn): self.role.delete_user(ref.role_dn, ref.user_dn, @@ -191,7 +190,7 @@ class Assignment(assignment.Driver): self.role.delete_user(ref.role_dn, ref.user_dn, self.role._dn_to_id(ref.role_dn)) - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): """Called when the group was deleted. Any role assignments for the group should be cleaned up. @@ -277,20 +276,39 @@ class Assignment(assignment.Driver): return self._roles_from_role_dicts(metadata_ref.get('roles', []), inherited_to_projects) - def list_role_assignments(self): + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): role_assignments = [] - for a in self.role.list_role_assignments(self.project.tree_dn): - if isinstance(a, UserRoleAssociation): - assignment = { - 'role_id': self.role._dn_to_id(a.role_dn), - 'user_id': self.user._dn_to_id(a.user_dn), - 'project_id': self.project._dn_to_id(a.project_dn)} - else: - assignment = { - 'role_id': self.role._dn_to_id(a.role_dn), - 'group_id': self.group._dn_to_id(a.group_dn), - 'project_id': self.project._dn_to_id(a.project_dn)} - role_assignments.append(assignment) + + # Since the LDAP backend does not support assignments to domains, if + # the request is to filter by domain, then the answer is guaranteed + # to be an empty list. + if not domain_id: + for a in self.role.list_role_assignments(self.project.tree_dn): + if isinstance(a, UserRoleAssociation): + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'user_id': self.user._dn_to_id(a.user_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + else: + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'group_id': self.group._dn_to_id(a.group_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + + if role_id and assignment['role_id'] != role_id: + continue + if user_id and assignment.get('user_id') != user_id: + continue + if group_ids and assignment.get('group_id') not in group_ids: + continue + if project_ids and assignment['project_id'] not in project_ids: + continue + + role_assignments.append(assignment) + return role_assignments def delete_project_assignments(self, project_id): @@ -313,9 +331,7 @@ class ProjectApi(common_ldap.ProjectLdapStructureMixin, or self.DEFAULT_MEMBER_ATTRIBUTE) def get_user_projects(self, user_dn, associations): - """Returns list of tenants a user has access to - """ - + """Returns the list of tenants to which a user has access.""" project_ids = set() for assoc in associations: project_ids.add(self._dn_to_id(assoc.project_dn)) @@ -497,9 +513,7 @@ class RoleApi(ldap_role.RoleLdapStructureMixin, common_ldap.BaseLdap): self.id_attr: role_id}) def list_role_assignments(self, project_tree_dn): - """Returns a list of all the role assignments linked to project_tree_dn - attribute. - """ + """List the role assignments linked to project_tree_dn attribute.""" try: roles = self._ldap_get_list(project_tree_dn, ldap.SCOPE_SUBTREE, attrlist=[self.member_attribute]) diff --git a/keystone-moon/keystone/assignment/backends/sql.py b/keystone-moon/keystone/assignment/backends/sql.py index 2de6ca60..89ff64b5 100644 --- a/keystone-moon/keystone/assignment/backends/sql.py +++ b/keystone-moon/keystone/assignment/backends/sql.py @@ -14,7 +14,6 @@ from oslo_config import cfg from oslo_log import log -import six import sqlalchemy from sqlalchemy.sql.expression import false @@ -53,10 +52,10 @@ class AssignmentType(object): class Assignment(keystone_assignment.Driver): def default_role_driver(self): - return "keystone.assignment.role_backends.sql.Role" + return 'sql' def default_resource_driver(self): - return 'keystone.resource.backends.sql.Resource' + return 'sql' def list_user_ids_for_project(self, tenant_id): with sql.transaction() as session: @@ -336,7 +335,62 @@ class Assignment(keystone_assignment.Driver): 'Cannot remove role that has not been granted, %s') % role_id) - def list_role_assignments(self): + def _get_user_assignment_types(self): + return [AssignmentType.USER_PROJECT, AssignmentType.USER_DOMAIN] + + def _get_group_assignment_types(self): + return [AssignmentType.GROUP_PROJECT, AssignmentType.GROUP_DOMAIN] + + def _get_project_assignment_types(self): + return [AssignmentType.USER_PROJECT, AssignmentType.GROUP_PROJECT] + + def _get_domain_assignment_types(self): + return [AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN] + + def _get_assignment_types(self, user, group, project, domain): + """Returns a list of role assignment types based on provided entities + + If one of user or group (the "actor") as well as one of project or + domain (the "target") are provided, the list will contain the role + assignment type for that specific pair of actor and target. + + If only an actor or target is provided, the list will contain the + role assignment types that satisfy the specified entity. + + For example, if user and project are provided, the return will be: + + [AssignmentType.USER_PROJECT] + + However, if only user was provided, the return would be: + + [AssignmentType.USER_PROJECT, AssignmentType.USER_DOMAIN] + + It is not expected that user and group (or project and domain) are + specified - but if they are, the most fine-grained value will be + chosen (i.e. user over group, project over domain). + + """ + actor_types = [] + if user: + actor_types = self._get_user_assignment_types() + elif group: + actor_types = self._get_group_assignment_types() + + target_types = [] + if project: + target_types = self._get_project_assignment_types() + elif domain: + target_types = self._get_domain_assignment_types() + + if actor_types and target_types: + return list(set(actor_types).intersection(target_types)) + + return actor_types or target_types + + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): def denormalize_role(ref): assignment = {} @@ -362,8 +416,35 @@ class Assignment(keystone_assignment.Driver): return assignment with sql.transaction() as session: - refs = session.query(RoleAssignment).all() - return [denormalize_role(ref) for ref in refs] + assignment_types = self._get_assignment_types( + user_id, group_ids, project_ids, domain_id) + + targets = None + if project_ids: + targets = project_ids + elif domain_id: + targets = [domain_id] + + actors = None + if group_ids: + actors = group_ids + elif user_id: + actors = [user_id] + + query = session.query(RoleAssignment) + + if role_id: + query = query.filter_by(role_id=role_id) + if actors: + query = query.filter(RoleAssignment.actor_id.in_(actors)) + if targets: + query = query.filter(RoleAssignment.target_id.in_(targets)) + if assignment_types: + query = query.filter(RoleAssignment.type.in_(assignment_types)) + if inherited_to_projects is not None: + query = query.filter_by(inherited=inherited_to_projects) + + return [denormalize_role(ref) for ref in query.all()] def delete_project_assignments(self, project_id): with sql.transaction() as session: @@ -377,13 +458,13 @@ class Assignment(keystone_assignment.Driver): q = q.filter_by(role_id=role_id) q.delete(False) - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): with sql.transaction() as session: q = session.query(RoleAssignment) q = q.filter_by(actor_id=user_id) q.delete(False) - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): with sql.transaction() as session: q = session.query(RoleAssignment) q = q.filter_by(actor_id=group_id) @@ -399,12 +480,15 @@ class RoleAssignment(sql.ModelBase, sql.DictBase): AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN, name='type'), nullable=False) - actor_id = sql.Column(sql.String(64), nullable=False, index=True) + actor_id = sql.Column(sql.String(64), nullable=False) target_id = sql.Column(sql.String(64), nullable=False) role_id = sql.Column(sql.String(64), nullable=False) inherited = sql.Column(sql.Boolean, default=False, nullable=False) - __table_args__ = (sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', - 'role_id'), {}) + __table_args__ = ( + sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', 'role_id', + 'inherited'), + sql.Index('ix_actor_id', 'actor_id'), + ) def to_dict(self): """Override parent to_dict() method with a simpler implementation. @@ -412,4 +496,4 @@ class RoleAssignment(sql.ModelBase, sql.DictBase): RoleAssignment doesn't have non-indexed 'extra' attributes, so the parent implementation is not applicable. """ - return dict(six.iteritems(self)) + return dict(self.items()) diff --git a/keystone-moon/keystone/assignment/controllers.py b/keystone-moon/keystone/assignment/controllers.py index ff27fd36..d33dce70 100644 --- a/keystone-moon/keystone/assignment/controllers.py +++ b/keystone-moon/keystone/assignment/controllers.py @@ -15,7 +15,6 @@ """Workflow Logic the Assignment service.""" -import copy import functools import uuid @@ -26,10 +25,10 @@ from six.moves import urllib from keystone.assignment import schema from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import validation from keystone import exception -from keystone.i18n import _, _LW -from keystone.models import token_model +from keystone.i18n import _ from keystone import notifications @@ -51,18 +50,11 @@ class TenantAssignment(controller.V2Controller): Doesn't care about token scopedness. """ - try: - token_data = self.token_provider_api.validate_token( - context['token_id']) - token_ref = token_model.KeystoneToken(token_id=context['token_id'], - token_data=token_data) - except exception.NotFound as e: - LOG.warning(_LW('Authentication failed: %s'), e) - raise exception.Unauthorized(e) + token_ref = utils.get_token_ref(context) tenant_refs = ( self.assignment_api.list_projects_for_user(token_ref.user_id)) - tenant_refs = [self.filter_domain_id(ref) for ref in tenant_refs + tenant_refs = [self.v3_to_v2_project(ref) for ref in tenant_refs if ref['domain_id'] == CONF.identity.default_domain_id] params = { 'limit': context['query_string'].get('limit'), @@ -107,7 +99,14 @@ class Role(controller.V2Controller): msg = _('Name field is required and cannot be empty') raise exception.ValidationError(message=msg) - role_id = uuid.uuid4().hex + if role['name'] == CONF.member_role_name: + # Use the configured member role ID when creating the configured + # member role name. This avoids the potential of creating a + # "member" role with an unexpected ID. + role_id = CONF.member_role_id + else: + role_id = uuid.uuid4().hex + role['id'] = role_id role_ref = self.role_api.create_role(role_id, role) return {'role': role_ref} @@ -152,8 +151,8 @@ class RoleAssignmentV2(controller.V2Controller): """ self.assert_admin(context) if tenant_id is None: - raise exception.NotImplemented(message='User roles not supported: ' - 'tenant_id required') + raise exception.NotImplemented( + message=_('User roles not supported: tenant_id required')) self.assignment_api.add_role_to_user_and_project( user_id, tenant_id, role_id) @@ -171,8 +170,8 @@ class RoleAssignmentV2(controller.V2Controller): """ self.assert_admin(context) if tenant_id is None: - raise exception.NotImplemented(message='User roles not supported: ' - 'tenant_id required') + raise exception.NotImplemented( + message=_('User roles not supported: tenant_id required')) # This still has the weird legacy semantics that adding a role to # a user also adds them to a tenant, so we must follow up on that @@ -282,7 +281,16 @@ class RoleV3(controller.V3Controller): @controller.protected() @validation.validated(schema.role_create, 'role') def create_role(self, context, role): - ref = self._assign_unique_id(self._normalize_dict(role)) + if role['name'] == CONF.member_role_name: + # Use the configured member role ID when creating the configured + # member role name. This avoids the potential of creating a + # "member" role with an unexpected ID. + role['id'] = CONF.member_role_id + else: + role = self._assign_unique_id(role) + + ref = self._normalize_dict(role) + initiator = notifications._get_request_audit_info(context) ref = self.role_api.create_role(ref['id'], ref, initiator) return RoleV3.wrap_member(context, ref) @@ -452,16 +460,25 @@ class RoleAssignmentV3(controller.V3Controller): actor (e.g. user or group), target (e.g. domain or project) and role. If it is an inherited role, then this is also indicated. Examples: + For a non-inherited expanded assignment from group membership: {'user_id': user_id, - 'project_id': domain_id, - 'role_id': role_id} + 'project_id': project_id, + 'role_id': role_id, + 'indirect': {'group_id': group_id}} - or, for an inherited role: + or, for a project inherited role: {'user_id': user_id, - 'domain_id': domain_id, + 'project_id': project_id, 'role_id': role_id, - 'inherited_to_projects': true} + 'indirect': {'project_id': parent_id}} + + It is possible to deduce if a role assignment came from group + membership if it has both 'user_id' in the main body of the dict and + 'group_id' in the 'indirect' subdict, as well as it is possible to + deduce if it has come from inheritance if it contains both a + 'project_id' in the main body of the dict and 'parent_id' in the + 'indirect' subdict. This function maps this into the format to be returned via the API, e.g. for the second example above: @@ -471,262 +488,71 @@ class RoleAssignmentV3(controller.V3Controller): {'id': user_id} }, 'scope': { - 'domain': { - {'id': domain_id} + 'project': { + {'id': project_id} }, - 'OS-INHERIT:inherited_to': 'projects + 'OS-INHERIT:inherited_to': 'projects' }, 'role': { {'id': role_id} }, 'links': { - 'assignment': '/domains/domain_id/users/user_id/roles/' - 'role_id/inherited_to_projects' + 'assignment': '/OS-INHERIT/projects/parent_id/users/user_id/' + 'roles/role_id/inherited_to_projects' } } """ - formatted_entity = {} - suffix = "" - if 'user_id' in entity: - formatted_entity['user'] = {'id': entity['user_id']} - actor_link = 'users/%s' % entity['user_id'] - if 'group_id' in entity: - formatted_entity['group'] = {'id': entity['group_id']} - actor_link = 'groups/%s' % entity['group_id'] - if 'role_id' in entity: - formatted_entity['role'] = {'id': entity['role_id']} + formatted_entity = {'links': {}} + inherited_assignment = entity.get('inherited_to_projects') + if 'project_id' in entity: formatted_entity['scope'] = ( {'project': {'id': entity['project_id']}}) - if 'inherited_to_projects' in entity: - formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( - 'projects') - target_link = '/OS-INHERIT/projects/%s' % entity['project_id'] - suffix = '/inherited_to_projects' - else: - target_link = '/projects/%s' % entity['project_id'] - if 'domain_id' in entity: - formatted_entity['scope'] = ( - {'domain': {'id': entity['domain_id']}}) - if 'inherited_to_projects' in entity: - formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( - 'projects') - target_link = '/OS-INHERIT/domains/%s' % entity['domain_id'] - suffix = '/inherited_to_projects' - else: - target_link = '/domains/%s' % entity['domain_id'] - formatted_entity.setdefault('links', {}) - - path = '%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % { - 'target': target_link, - 'actor': actor_link, - 'role': entity['role_id'], - 'suffix': suffix} - formatted_entity['links']['assignment'] = self.base_url(context, path) - - return formatted_entity - def _expand_indirect_assignments(self, context, refs): - """Processes entity list into all-direct assignments. - - For any group role assignments in the list, create a role assignment - entity for each member of that group, and then remove the group - assignment entity itself from the list. + if 'domain_id' in entity.get('indirect', {}): + inherited_assignment = True + formatted_link = ('/domains/%s' % + entity['indirect']['domain_id']) + elif 'project_id' in entity.get('indirect', {}): + inherited_assignment = True + formatted_link = ('/projects/%s' % + entity['indirect']['project_id']) + else: + formatted_link = '/projects/%s' % entity['project_id'] + elif 'domain_id' in entity: + formatted_entity['scope'] = {'domain': {'id': entity['domain_id']}} + formatted_link = '/domains/%s' % entity['domain_id'] - If the OS-INHERIT extension is enabled, then honor any inherited - roles on the domain by creating the equivalent on all projects - owned by the domain. + if 'user_id' in entity: + formatted_entity['user'] = {'id': entity['user_id']} - For any new entity created by virtue of group membership, add in an - additional link to that membership. + if 'group_id' in entity.get('indirect', {}): + membership_url = ( + self.base_url(context, '/groups/%s/users/%s' % ( + entity['indirect']['group_id'], entity['user_id']))) + formatted_entity['links']['membership'] = membership_url + formatted_link += '/groups/%s' % entity['indirect']['group_id'] + else: + formatted_link += '/users/%s' % entity['user_id'] + elif 'group_id' in entity: + formatted_entity['group'] = {'id': entity['group_id']} + formatted_link += '/groups/%s' % entity['group_id'] - """ - def _get_group_members(ref): - """Get a list of group members. + formatted_entity['role'] = {'id': entity['role_id']} + formatted_link += '/roles/%s' % entity['role_id'] - Get the list of group members. If this fails with - GroupNotFound, then log this as a warning, but allow - overall processing to continue. + if inherited_assignment: + formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( + 'projects') + formatted_link = ('/OS-INHERIT%s/inherited_to_projects' % + formatted_link) - """ - try: - members = self.identity_api.list_users_in_group( - ref['group']['id']) - except exception.GroupNotFound: - members = [] - # The group is missing, which should not happen since - # group deletion should remove any related assignments, so - # log a warning - target = 'Unknown' - # Should always be a domain or project, but since to get - # here things have gone astray, let's be cautious. - if 'scope' in ref: - if 'domain' in ref['scope']: - dom_id = ref['scope']['domain'].get('id', 'Unknown') - target = 'Domain: %s' % dom_id - elif 'project' in ref['scope']: - proj_id = ref['scope']['project'].get('id', 'Unknown') - target = 'Project: %s' % proj_id - role_id = 'Unknown' - if 'role' in ref and 'id' in ref['role']: - role_id = ref['role']['id'] - LOG.warning( - _LW('Group %(group)s not found for role-assignment - ' - '%(target)s with Role: %(role)s'), { - 'group': ref['group']['id'], 'target': target, - 'role': role_id}) - return members - - def _build_user_assignment_equivalent_of_group( - user, group_id, template): - """Create a user assignment equivalent to the group one. - - The template has had the 'group' entity removed, so - substitute a 'user' one. The 'assignment' link stays as it is, - referring to the group assignment that led to this role. - A 'membership' link is added that refers to this particular - user's membership of this group. - - """ - user_entry = copy.deepcopy(template) - user_entry['user'] = {'id': user['id']} - user_entry['links']['membership'] = ( - self.base_url(context, '/groups/%s/users/%s' % - (group_id, user['id']))) - return user_entry - - def _build_project_equivalent_of_user_target_role( - project_id, target_id, target_type, template): - """Create a user project assignment equivalent to the domain one. - - The template has had the 'domain' entity removed, so - substitute a 'project' one, modifying the 'assignment' link - to match. - - """ - project_entry = copy.deepcopy(template) - project_entry['scope']['project'] = {'id': project_id} - project_entry['links']['assignment'] = ( - self.base_url( - context, - '/OS-INHERIT/%s/%s/users/%s/roles/%s' - '/inherited_to_projects' % ( - target_type, target_id, project_entry['user']['id'], - project_entry['role']['id']))) - return project_entry - - def _build_project_equivalent_of_group_target_role( - user_id, group_id, project_id, - target_id, target_type, template): - """Create a user project equivalent to the domain group one. - - The template has had the 'domain' and 'group' entities removed, so - substitute a 'user-project' one, modifying the 'assignment' link - to match. - - """ - project_entry = copy.deepcopy(template) - project_entry['user'] = {'id': user_id} - project_entry['scope']['project'] = {'id': project_id} - project_entry['links']['assignment'] = ( - self.base_url(context, - '/OS-INHERIT/%s/%s/groups/%s/roles/%s' - '/inherited_to_projects' % ( - target_type, target_id, group_id, - project_entry['role']['id']))) - project_entry['links']['membership'] = ( - self.base_url(context, '/groups/%s/users/%s' % - (group_id, user_id))) - return project_entry - - # Scan the list of entities for any assignments that need to be - # expanded. - # - # If the OS-INERIT extension is enabled, the refs lists may - # contain roles to be inherited from domain to project, so expand - # these as well into project equivalents - # - # For any regular group entries, expand these into user entries based - # on membership of that group. - # - # Due to the potentially large expansions, rather than modify the - # list we are enumerating, we build a new one as we go. - # - - new_refs = [] - for r in refs: - if 'OS-INHERIT:inherited_to' in r['scope']: - if 'domain' in r['scope']: - # It's an inherited domain role - so get the list of - # projects owned by this domain. - project_ids = ( - [x['id'] for x in - self.resource_api.list_projects_in_domain( - r['scope']['domain']['id'])]) - base_entry = copy.deepcopy(r) - target_type = 'domains' - target_id = base_entry['scope']['domain']['id'] - base_entry['scope'].pop('domain') - else: - # It's an inherited project role - so get the list of - # projects in this project subtree. - project_id = r['scope']['project']['id'] - project_ids = ( - [x['id'] for x in - self.resource_api.list_projects_in_subtree( - project_id)]) - base_entry = copy.deepcopy(r) - target_type = 'projects' - target_id = base_entry['scope']['project']['id'] - base_entry['scope'].pop('project') - - # For each project, create an equivalent role assignment - for p in project_ids: - # If it's a group assignment, then create equivalent user - # roles based on membership of the group - if 'group' in base_entry: - members = _get_group_members(base_entry) - sub_entry = copy.deepcopy(base_entry) - group_id = sub_entry['group']['id'] - sub_entry.pop('group') - for m in members: - new_entry = ( - _build_project_equivalent_of_group_target_role( - m['id'], group_id, p, - target_id, target_type, sub_entry)) - new_refs.append(new_entry) - else: - new_entry = ( - _build_project_equivalent_of_user_target_role( - p, target_id, target_type, base_entry)) - new_refs.append(new_entry) - elif 'group' in r: - # It's a non-inherited group role assignment, so get the list - # of members. - members = _get_group_members(r) - - # Now replace that group role assignment entry with an - # equivalent user role assignment for each of the group members - base_entry = copy.deepcopy(r) - group_id = base_entry['group']['id'] - base_entry.pop('group') - for m in members: - user_entry = _build_user_assignment_equivalent_of_group( - m, group_id, base_entry) - new_refs.append(user_entry) - else: - new_refs.append(r) + formatted_entity['links']['assignment'] = self.base_url(context, + formatted_link) - return new_refs - - def _filter_inherited(self, entry): - if ('inherited_to_projects' in entry and - not CONF.os_inherit.enabled): - return False - else: - return True + return formatted_entity def _assert_effective_filters(self, inherited, group, domain): """Assert that useless filter combinations are avoided. @@ -762,13 +588,28 @@ class RoleAssignmentV3(controller.V3Controller): 'scope.domain.id', 'scope.project.id', 'scope.OS-INHERIT:inherited_to', 'user.id') def list_role_assignments(self, context, filters): + """List role assignments to user and groups on domains and projects. + + Return a list of all existing role assignments in the system, filtered + by assignments attributes, if provided. - # TODO(henry-nash): This implementation uses the standard filtering - # in the V3.wrap_collection. Given the large number of individual - # assignments, this is pretty inefficient. An alternative would be - # to pass the filters into the driver call, so that the list size is - # kept a minimum. + If effective option is used and OS-INHERIT extension is enabled, the + following functions will be applied: + 1) For any group role assignment on a target, replace it by a set of + role assignments containing one for each user of that group on that + target; + 2) For any inherited role assignment for an actor on a target, replace + it by a set of role assignments for that actor on every project under + that target. + It means that, if effective mode is used, no group or domain inherited + assignments will be present in the resultant list. Thus, combining + effective with them is invalid. + + As a role assignment contains only one actor and one target, providing + both user and group ids or domain and project ids is invalid as well. + + """ params = context['query_string'] effective = 'effective' in params and ( self.query_filter_is_true(params['effective'])) @@ -791,17 +632,17 @@ class RoleAssignmentV3(controller.V3Controller): domain=params.get( 'scope.domain.id')) - hints = self.build_driver_hints(context, filters) - refs = self.assignment_api.list_role_assignments() - formatted_refs = ( - [self._format_entity(context, x) for x in refs - if self._filter_inherited(x)]) + refs = self.assignment_api.list_role_assignments( + role_id=params.get('role.id'), + user_id=params.get('user.id'), + group_id=params.get('group.id'), + domain_id=params.get('scope.domain.id'), + project_id=params.get('scope.project.id'), + inherited=inherited, effective=effective) - if effective: - formatted_refs = self._expand_indirect_assignments(context, - formatted_refs) + formatted_refs = [self._format_entity(context, ref) for ref in refs] - return self.wrap_collection(context, formatted_refs, hints=hints) + return self.wrap_collection(context, formatted_refs) @controller.protected() def get_role_assignment(self, context): diff --git a/keystone-moon/keystone/assignment/core.py b/keystone-moon/keystone/assignment/core.py index 0f9c03e9..a001e6b1 100644 --- a/keystone-moon/keystone/assignment/core.py +++ b/keystone-moon/keystone/assignment/core.py @@ -12,9 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the assignment service.""" +"""Main entry point into the Assignment service.""" import abc +import copy from oslo_config import cfg from oslo_log import log @@ -28,7 +29,6 @@ from keystone import exception from keystone.i18n import _ from keystone.i18n import _LI from keystone import notifications -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -36,40 +36,6 @@ LOG = log.getLogger(__name__) MEMOIZE = cache.get_memoization_decorator(section='role') -def deprecated_to_role_api(f): - """Specialized deprecation wrapper for assignment to role api. - - This wraps the standard deprecation wrapper and fills in the method - names automatically. - - """ - @six.wraps(f) - def wrapper(*args, **kwargs): - x = versionutils.deprecated( - what='assignment.' + f.__name__ + '()', - as_of=versionutils.deprecated.KILO, - in_favor_of='role.' + f.__name__ + '()') - return x(f) - return wrapper() - - -def deprecated_to_resource_api(f): - """Specialized deprecation wrapper for assignment to resource api. - - This wraps the standard deprecation wrapper and fills in the method - names automatically. - - """ - @six.wraps(f) - def wrapper(*args, **kwargs): - x = versionutils.deprecated( - what='assignment.' + f.__name__ + '()', - as_of=versionutils.deprecated.KILO, - in_favor_of='resource.' + f.__name__ + '()') - return x(f) - return wrapper() - - @dependency.provider('assignment_api') @dependency.requires('credential_api', 'identity_api', 'resource_api', 'revoke_api', 'role_api') @@ -80,6 +46,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.assignment' + _PROJECT = 'project' _ROLE_REMOVED_FROM_USER = 'role_removed_from_user' _INVALIDATION_USER_PROJECT_TOKENS = 'invalidate_user_project_tokens' @@ -129,7 +98,7 @@ class Manager(manager.Manager): """ def _get_group_project_roles(user_id, project_ref): group_ids = self._get_group_ids_for_user_id(user_id) - return self.driver.list_role_ids_for_groups_on_project( + return self.list_role_ids_for_groups_on_project( group_ids, project_ref['id'], project_ref['domain_id'], @@ -155,7 +124,8 @@ class Manager(manager.Manager): except (exception.MetadataNotFound, exception.NotImplemented): pass # As well inherited roles from parent projects - for p in self.list_project_parents(project_ref['id']): + for p in self.resource_api.list_project_parents( + project_ref['id']): p_roles = self.list_grants( user_id=user_id, project_id=p['id'], inherited_to_projects=True) @@ -207,7 +177,7 @@ class Manager(manager.Manager): return self._roles_from_role_dicts( metadata_ref.get('roles', {}), False) - self.get_domain(domain_id) + self.resource_api.get_domain(domain_id) user_role_list = _get_user_domain_roles(user_id, domain_id) group_role_list = _get_group_domain_roles(user_id, domain_id) # Use set() to process the list to remove any duplicates @@ -218,11 +188,11 @@ class Manager(manager.Manager): if project_id is not None: project = self.resource_api.get_project(project_id) - role_ids = self.driver.list_role_ids_for_groups_on_project( + role_ids = self.list_role_ids_for_groups_on_project( group_ids, project_id, project['domain_id'], self._list_parent_ids_of_project(project_id)) elif domain_id is not None: - role_ids = self.driver.list_role_ids_for_groups_on_domain( + role_ids = self.list_role_ids_for_groups_on_domain( group_ids, domain_id) else: raise AttributeError(_("Must specify either domain or project")) @@ -261,10 +231,24 @@ class Manager(manager.Manager): tenant_id, CONF.member_role_id) - def add_role_to_user_and_project(self, user_id, tenant_id, role_id): - self.resource_api.get_project(tenant_id) + @notifications.role_assignment('created') + def _add_role_to_user_and_project_adapter(self, role_id, user_id=None, + group_id=None, domain_id=None, + project_id=None, + inherited_to_projects=False, + context=None): + + # The parameters for this method must match the parameters for + # create_grant so that the notifications.role_assignment decorator + # will work. + + self.resource_api.get_project(project_id) self.role_api.get_role(role_id) - self.driver.add_role_to_user_and_project(user_id, tenant_id, role_id) + self.driver.add_role_to_user_and_project(user_id, project_id, role_id) + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + self._add_role_to_user_and_project_adapter( + role_id, user_id=user_id, project_id=tenant_id) def remove_user_from_project(self, tenant_id, user_id): """Remove user from a tenant @@ -299,7 +283,7 @@ class Manager(manager.Manager): # optimization with the various backend technologies (SQL, LDAP etc.). group_ids = self._get_group_ids_for_user_id(user_id) - project_ids = self.driver.list_project_ids_for_user( + project_ids = self.list_project_ids_for_user( user_id, group_ids, hints or driver_hints.Hints()) if not CONF.os_inherit.enabled: @@ -309,7 +293,7 @@ class Manager(manager.Manager): # inherited role (direct or group) on any parent project, in which # case we must add in all the projects in that parent's subtree. project_ids = set(project_ids) - project_ids_inherited = self.driver.list_project_ids_for_user( + project_ids_inherited = self.list_project_ids_for_user( user_id, group_ids, hints or driver_hints.Hints(), inherited=True) for proj_id in project_ids_inherited: project_ids.update( @@ -317,7 +301,7 @@ class Manager(manager.Manager): self.resource_api.list_projects_in_subtree(proj_id))) # Now do the same for any domain inherited roles - domain_ids = self.driver.list_domain_ids_for_user( + domain_ids = self.list_domain_ids_for_user( user_id, group_ids, hints or driver_hints.Hints(), inherited=True) project_ids.update( @@ -335,33 +319,42 @@ class Manager(manager.Manager): # projects for a user is pushed down into the driver to enable # optimization with the various backend technologies (SQL, LDAP etc.). group_ids = self._get_group_ids_for_user_id(user_id) - domain_ids = self.driver.list_domain_ids_for_user( + domain_ids = self.list_domain_ids_for_user( user_id, group_ids, hints or driver_hints.Hints()) return self.resource_api.list_domains_from_ids(domain_ids) def list_domains_for_groups(self, group_ids): - domain_ids = self.driver.list_domain_ids_for_groups(group_ids) + domain_ids = self.list_domain_ids_for_groups(group_ids) return self.resource_api.list_domains_from_ids(domain_ids) def list_projects_for_groups(self, group_ids): project_ids = ( - self.driver.list_project_ids_for_groups(group_ids, - driver_hints.Hints())) + self.list_project_ids_for_groups(group_ids, driver_hints.Hints())) if not CONF.os_inherit.enabled: return self.resource_api.list_projects_from_ids(project_ids) - # Inherited roles are enabled, so check to see if these groups have any - # roles on any domain, in which case we must add in all the projects - # in that domain. + # os_inherit extension is enabled, so check to see if these groups have + # any inherited role assignment on: i) any domain, in which case we + # must add in all the projects in that domain; ii) any project, in + # which case we must add in all the subprojects under that project in + # the hierarchy. - domain_ids = self.driver.list_domain_ids_for_groups( - group_ids, inherited=True) + domain_ids = self.list_domain_ids_for_groups(group_ids, inherited=True) project_ids_from_domains = ( self.resource_api.list_project_ids_from_domain_ids(domain_ids)) + parents_ids = self.list_project_ids_for_groups(group_ids, + driver_hints.Hints(), + inherited=True) + + subproject_ids = [] + for parent_id in parents_ids: + subtree = self.resource_api.list_projects_in_subtree(parent_id) + subproject_ids += [subproject['id'] for subproject in subtree] + return self.resource_api.list_projects_from_ids( - list(set(project_ids + project_ids_from_domains))) + list(set(project_ids + project_ids_from_domains + subproject_ids))) def list_role_assignments_for_role(self, role_id=None): # NOTE(henry-nash): Currently the efficiency of the key driver @@ -374,17 +367,37 @@ class Manager(manager.Manager): return [r for r in self.driver.list_role_assignments() if r['role_id'] == role_id] - 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, + @notifications.role_assignment('deleted') + def _remove_role_from_user_and_project_adapter(self, role_id, user_id=None, + group_id=None, + domain_id=None, + project_id=None, + inherited_to_projects=False, + context=None): + + # The parameters for this method must match the parameters for + # delete_grant so that the notifications.role_assignment decorator + # will work. + + self.driver.remove_role_from_user_and_project(user_id, project_id, role_id) self.identity_api.emit_invalidate_user_token_persistence(user_id) self.revoke_api.revoke_by_grant(role_id, user_id=user_id, - project_id=tenant_id) + project_id=project_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) @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE) def _emit_invalidate_user_token_persistence(self, user_id): self.identity_api.emit_invalidate_user_token_persistence(user_id) + def _emit_invalidate_grant_token_persistence(self, user_id, project_id): + self.identity_api.emit_invalidate_grant_token_persistence( + {'user_id': user_id, 'project_id': project_id} + ) + @notifications.role_assignment('created') def create_grant(self, role_id, user_id=None, group_id=None, domain_id=None, project_id=None, @@ -405,7 +418,7 @@ class Manager(manager.Manager): self.resource_api.get_domain(domain_id) if project_id: self.resource_api.get_project(project_id) - self.driver.check_grant_role_id( + self.check_grant_role_id( role_id, user_id, group_id, domain_id, project_id, inherited_to_projects) return role_ref @@ -417,11 +430,15 @@ class Manager(manager.Manager): self.resource_api.get_domain(domain_id) if project_id: self.resource_api.get_project(project_id) - grant_ids = self.driver.list_grant_role_ids( + grant_ids = self.list_grant_role_ids( user_id, group_id, domain_id, project_id, inherited_to_projects) return self.role_api.list_roles_from_ids(grant_ids) @notifications.role_assignment('deleted') + 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) + def delete_grant(self, role_id, user_id=None, group_id=None, domain_id=None, project_id=None, inherited_to_projects=False, context=None): @@ -430,17 +447,29 @@ class Manager(manager.Manager): role_id=role_id, domain_id=domain_id, project_id=project_id) + self._emit_revoke_user_grant( + role_id, user_id, domain_id, project_id, + inherited_to_projects, context) 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): - if user['id'] != user_id: - self._emit_invalidate_user_token_persistence( - user['id']) - self.revoke_api.revoke_by_grant( - user_id=user['id'], role_id=role_id, - domain_id=domain_id, project_id=project_id) + # Group may contain a lot of users so revocation will be + # by role & domain/project + if domain_id is None: + self.revoke_api.revoke_by_project_role_assignment( + project_id, role_id + ) + else: + self.revoke_api.revoke_by_domain_role_assignment( + domain_id, role_id + ) + 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 self.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) except exception.GroupNotFound: LOG.debug('Group %s not found, no tokens to invalidate.', group_id) @@ -457,8 +486,356 @@ class Manager(manager.Manager): self.resource_api.get_project(project_id) self.driver.delete_grant(role_id, user_id, group_id, domain_id, project_id, inherited_to_projects) - if user_id is not None: - self._emit_invalidate_user_token_persistence(user_id) + + # The methods _expand_indirect_assignment, _list_direct_role_assignments + # and _list_effective_role_assignments below are only used on + # list_role_assignments, but they are not in its scope as nested functions + # since it would significantly increase McCabe complexity, that should be + # kept as it is in order to detect unnecessarily complex code, which is not + # this case. + + def _expand_indirect_assignment(self, ref, user_id=None, + project_id=None): + """Returns a list of expanded role assignments. + + This methods is called for each discovered assignment that either needs + a group assignment expanded into individual user assignments, or needs + an inherited assignment to be applied to its children. + + In all cases, if either user_id and/or project_id is specified, then we + filter the result on those values. + + """ + + def create_group_assignment(base_ref, user_id): + """Creates a group assignment from the provided ref.""" + + ref = copy.deepcopy(base_ref) + + ref['user_id'] = user_id + + indirect = ref.setdefault('indirect', {}) + indirect['group_id'] = ref.pop('group_id') + + return ref + + def expand_group_assignment(ref, user_id): + """Expands group role assignment. + + For any group role assignment on a target, it is replaced by a list + of role assignments containing one for each user of that group on + that target. + + An example of accepted ref is: + + { + 'group_id': group_id, + 'project_id': project_id, + 'role_id': role_id + } + + Once expanded, it should be returned as a list of entities like the + one below, one for each each user_id in the provided group_id. + + { + 'user_id': user_id, + 'project_id': project_id, + 'role_id': role_id, + 'indirect' : { + 'group_id': group_id + } + } + + Returned list will be formatted by the Controller, which will + deduce a role assignment came from group membership if it has both + 'user_id' in the main body of the dict and 'group_id' in indirect + subdict. + + """ + if user_id: + return [create_group_assignment(ref, user_id=user_id)] + + return [create_group_assignment(ref, user_id=m['id']) + for m in self.identity_api.list_users_in_group( + ref['group_id'])] + + def expand_inherited_assignment(ref, user_id, project_id=None): + """Expands inherited role assignments. + + If this is a group role assignment on a target, replace it by a + list of role assignments containing one for each user of that + group, on every project under that target. + + If this is a user role assignment on a target, replace it by a + list of role assignments for that user on every project under + that target. + + An example of accepted ref is: + + { + 'group_id': group_id, + 'project_id': parent_id, + 'role_id': role_id, + 'inherited_to_projects': 'projects' + } + + Once expanded, it should be returned as a list of entities like the + one below, one for each each user_id in the provided group_id and + for each subproject_id in the project_id subtree. + + { + 'user_id': user_id, + 'project_id': subproject_id, + 'role_id': role_id, + 'indirect' : { + 'group_id': group_id, + 'project_id': parent_id + } + } + + Returned list will be formatted by the Controller, which will + deduce a role assignment came from group membership if it has both + 'user_id' in the main body of the dict and 'group_id' in the + 'indirect' subdict, as well as it is possible to deduce if it has + come from inheritance if it contains both a 'project_id' in the + main body of the dict and 'parent_id' in the 'indirect' subdict. + + """ + def create_inherited_assignment(base_ref, project_id): + """Creates a project assignment from the provided ref. + + base_ref can either be a project or domain inherited + assignment ref. + + """ + ref = copy.deepcopy(base_ref) + + indirect = ref.setdefault('indirect', {}) + if ref.get('project_id'): + indirect['project_id'] = ref.pop('project_id') + else: + indirect['domain_id'] = ref.pop('domain_id') + + ref['project_id'] = project_id + ref.pop('inherited_to_projects') + + return ref + + # Define expanded project list to which to apply this assignment + if project_id: + # Since ref is an inherited assignment, it must have come from + # the domain or a parent. We only need apply it to the project + # requested. + project_ids = [project_id] + elif ref.get('domain_id'): + # A domain inherited assignment, so apply it to all projects + # in this domain + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_domain( + ref['domain_id'])]) + else: + # It must be a project assignment, so apply it to the subtree + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_subtree( + ref['project_id'])]) + + new_refs = [] + if 'group_id' in ref: + # Expand role assignment for all members and for all projects + for ref in expand_group_assignment(ref, user_id): + new_refs += [create_inherited_assignment(ref, proj_id) + for proj_id in project_ids] + else: + # Expand role assignment for all projects + new_refs += [create_inherited_assignment(ref, proj_id) + for proj_id in project_ids] + + return new_refs + + if ref.get('inherited_to_projects') == 'projects': + return expand_inherited_assignment(ref, user_id, project_id) + elif 'group_id' in ref: + return expand_group_assignment(ref, user_id) + return [ref] + + def _list_effective_role_assignments(self, role_id, user_id, group_id, + domain_id, project_id, inherited): + """List role assignments in effective mode. + + When using effective mode, besides the direct assignments, the indirect + ones that come from grouping or inheritance are retrieved and will then + be expanded. + + The resulting list of assignments will be filtered by the provided + parameters, although since we are in effective mode, group can never + act as a filter (since group assignments are expanded into user roles) + and domain can only be filter if we want non-inherited assignments, + since domains can't inherit assignments. + + The goal of this method is to only ask the driver for those + assignments as could effect the result based on the parameter filters + specified, hence avoiding retrieving a huge list. + + """ + + def list_role_assignments_for_actor( + role_id, inherited, user_id=None, + group_ids=None, project_id=None, domain_id=None): + """List role assignments for actor on target. + + List direct and indirect assignments for an actor, optionally + for a given target (i.e. project or domain). + + :param role_id: List for a specific role, can be None meaning all + roles + :param inherited: Indicates whether inherited assignments or only + direct assignments are required. If None, then + both are required. + :param user_id: If not None, list only assignments that affect this + user. + :param group_ids: A list of groups required. Only one of user_id + and group_ids can be specified + :param project_id: If specified, only include those assignments + that affect this project + :param domain_id: If specified, only include those assignments + that affect this domain - by definition this will + not include any inherited assignments + + :returns: List of assignments matching the criteria. Any inherited + or group assignments that could affect the resulting + response are included. + + """ + + # List direct project role assignments + project_ids = [project_id] if project_id else None + + non_inherited_refs = [] + if inherited is False or inherited is None: + # Get non inherited assignments + non_inherited_refs = self.driver.list_role_assignments( + role_id=role_id, domain_id=domain_id, + project_ids=project_ids, user_id=user_id, + group_ids=group_ids, inherited_to_projects=False) + + inherited_refs = [] + if inherited is True or inherited is None: + # Get inherited assignments + if project_id: + # If we are filtering by a specific project, then we can + # only get inherited assignments from its domain or from + # any of its parents. + + # List inherited assignments from the project's domain + proj_domain_id = self.resource_api.get_project( + project_id)['domain_id'] + inherited_refs += self.driver.list_role_assignments( + role_id=role_id, domain_id=proj_domain_id, + user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + + # And those assignments that could be inherited from the + # project's parents. + parent_ids = [project['id'] for project in + self.resource_api.list_project_parents( + project_id)] + if parent_ids: + inherited_refs += self.driver.list_role_assignments( + role_id=role_id, project_ids=parent_ids, + user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + else: + # List inherited assignments without filtering by target + inherited_refs = self.driver.list_role_assignments( + role_id=role_id, user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + + return non_inherited_refs + inherited_refs + + # If filtering by group or inherited domain assignment the list is + # guranteed to be empty + if group_id or (domain_id and inherited): + return [] + + # If filtering by domain, then only non-inherited assignments are + # relevant, since domains don't inherit assignments + inherited = False if domain_id else inherited + + # List user assignments + direct_refs = list_role_assignments_for_actor( + role_id=role_id, user_id=user_id, project_id=project_id, + domain_id=domain_id, inherited=inherited) + + # And those from the user's groups + group_refs = [] + if user_id: + group_ids = self._get_group_ids_for_user_id(user_id) + if group_ids: + group_refs = list_role_assignments_for_actor( + role_id=role_id, project_id=project_id, + group_ids=group_ids, domain_id=domain_id, + inherited=inherited) + + # Expand grouping and inheritance on retrieved role assignments + refs = [] + for ref in (direct_refs + group_refs): + refs += self._expand_indirect_assignment(ref=ref, user_id=user_id, + project_id=project_id) + + return refs + + def _list_direct_role_assignments(self, role_id, user_id, group_id, + domain_id, project_id, inherited): + """List role assignments without applying expansion. + + Returns a list of direct role assignments, where their attributes match + the provided filters. + + """ + group_ids = [group_id] if group_id else None + project_ids = [project_id] if project_id else None + + return self.driver.list_role_assignments( + role_id=role_id, user_id=user_id, group_ids=group_ids, + domain_id=domain_id, project_ids=project_ids, + inherited_to_projects=inherited) + + def list_role_assignments(self, role_id=None, user_id=None, group_id=None, + domain_id=None, project_id=None, inherited=None, + effective=None): + """List role assignments, honoring effective mode and provided filters. + + Returns a list of role assignments, where their attributes match the + provided filters (role_id, user_id, group_id, domain_id, project_id and + inherited). The inherited filter defaults to None, meaning to get both + non-inherited and inherited role assignments. + + If effective mode is specified, this means that rather than simply + return the assignments that match the filters, any group or + inheritance assignments will be expanded. Group assignments will + become assignments for all the users in that group, and inherited + assignments will be shown on the projects below the assignment point. + Think of effective mode as being the list of assignments that actually + affect a user, for example the roles that would be placed in a token. + + If OS-INHERIT extension is disabled or the used driver does not support + inherited roles retrieval, inherited role assignments will be ignored. + + """ + + if not CONF.os_inherit.enabled: + if inherited: + return [] + inherited = False + + if effective: + return self._list_effective_role_assignments( + role_id, user_id, group_id, domain_id, project_id, inherited) + else: + return self._list_direct_role_assignments( + role_id, user_id, group_id, domain_id, project_id, inherited) def delete_tokens_for_role_assignments(self, role_id): assignments = self.list_role_assignments_for_role(role_id=role_id) @@ -532,98 +909,6 @@ class Manager(manager.Manager): # from persistence if persistence is enabled. pass - @deprecated_to_role_api - def create_role(self, role_id, role): - return self.role_api.create_role(role_id, role) - - @deprecated_to_role_api - def get_role(self, role_id): - return self.role_api.get_role(role_id) - - @deprecated_to_role_api - def update_role(self, role_id, role): - return self.role_api.update_role(role_id, role) - - @deprecated_to_role_api - def delete_role(self, role_id): - return self.role_api.delete_role(role_id) - - @deprecated_to_role_api - def list_roles(self, hints=None): - return self.role_api.list_roles(hints=hints) - - @deprecated_to_resource_api - def create_project(self, project_id, project): - return self.resource_api.create_project(project_id, project) - - @deprecated_to_resource_api - def get_project_by_name(self, tenant_name, domain_id): - return self.resource_api.get_project_by_name(tenant_name, domain_id) - - @deprecated_to_resource_api - def get_project(self, project_id): - return self.resource_api.get_project(project_id) - - @deprecated_to_resource_api - def update_project(self, project_id, project): - return self.resource_api.update_project(project_id, project) - - @deprecated_to_resource_api - def delete_project(self, project_id): - return self.resource_api.delete_project(project_id) - - @deprecated_to_resource_api - def list_projects(self, hints=None): - return self.resource_api.list_projects(hints=hints) - - @deprecated_to_resource_api - def list_projects_in_domain(self, domain_id): - return self.resource_api.list_projects_in_domain(domain_id) - - @deprecated_to_resource_api - def create_domain(self, domain_id, domain): - return self.resource_api.create_domain(domain_id, domain) - - @deprecated_to_resource_api - def get_domain_by_name(self, domain_name): - return self.resource_api.get_domain_by_name(domain_name) - - @deprecated_to_resource_api - def get_domain(self, domain_id): - return self.resource_api.get_domain(domain_id) - - @deprecated_to_resource_api - def update_domain(self, domain_id, domain): - return self.resource_api.update_domain(domain_id, domain) - - @deprecated_to_resource_api - def delete_domain(self, domain_id): - return self.resource_api.delete_domain(domain_id) - - @deprecated_to_resource_api - def list_domains(self, hints=None): - return self.resource_api.list_domains(hints=hints) - - @deprecated_to_resource_api - def assert_domain_enabled(self, domain_id, domain=None): - return self.resource_api.assert_domain_enabled(domain_id, domain) - - @deprecated_to_resource_api - def assert_project_enabled(self, project_id, project=None): - return self.resource_api.assert_project_enabled(project_id, project) - - @deprecated_to_resource_api - def is_leaf_project(self, project_id): - return self.resource_api.is_leaf_project(project_id) - - @deprecated_to_resource_api - def list_project_parents(self, project_id, user_id=None): - return self.resource_api.list_project_parents(project_id, user_id) - - @deprecated_to_resource_api - def list_projects_in_subtree(self, project_id, user_id=None): - return self.resource_api.list_projects_in_subtree(project_id, user_id) - @six.add_metaclass(abc.ABCMeta) class Driver(object): @@ -642,26 +927,6 @@ class Driver(object): role_list.append(d['id']) return role_list - def _add_role_to_role_dicts(self, role_id, inherited, dict_list, - allow_existing=True): - # There is a difference in error semantics when trying to - # assign a role that already exists between the coded v2 and v3 - # API calls. v2 will error if the assignment already exists, - # while v3 is silent. Setting the 'allow_existing' parameter - # appropriately lets this call be used for both. - role_set = set([frozenset(r.items()) for r in dict_list]) - key = frozenset(self._role_to_dict(role_id, inherited).items()) - if not allow_existing and key in role_set: - raise KeyError - role_set.add(key) - return [dict(r) for r in role_set] - - def _remove_role_from_role_dicts(self, role_id, inherited, dict_list): - role_set = set([frozenset(r.items()) for r in dict_list]) - role_set.remove(frozenset(self._role_to_dict(role_id, - inherited).items())) - return [dict(r) for r in role_set] - def _get_list_limit(self): return CONF.assignment.list_limit or CONF.list_limit @@ -740,8 +1005,16 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def list_role_assignments(self): + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): + """Returns a list of role assignments for actors on targets. + + Available parameters represent values in which the returned role + assignments attributes need to be filtered on. + """ raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod @@ -866,12 +1139,8 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover - # TODO(henry-nash): Rename the following two methods to match the more - # meaningfully named ones above. - -# TODO(ayoung): determine what else these two functions raise @abc.abstractmethod - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): """Deletes all assignments for a user. :raises: keystone.exception.RoleNotFound @@ -880,7 +1149,7 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): """Deletes all assignments for a group. :raises: keystone.exception.RoleNotFound @@ -894,6 +1163,8 @@ class Driver(object): class RoleManager(manager.Manager): """Default pivot point for the Role backend.""" + driver_namespace = 'keystone.role' + _ROLE = 'role' def __init__(self): @@ -902,9 +1173,8 @@ class RoleManager(manager.Manager): role_driver = CONF.role.driver if role_driver is None: - assignment_driver = ( - dependency.get_provider('assignment_api').driver) - role_driver = assignment_driver.default_role_driver() + assignment_manager = dependency.get_provider('assignment_api') + role_driver = assignment_manager.default_role_driver() super(RoleManager, self).__init__(role_driver) -- cgit 1.2.3-korg