From dc212cd4d2fa5fe0034f21bfd2a839283749b835 Mon Sep 17 00:00:00 2001 From: Tom Cocozzello Date: Wed, 25 Nov 2015 10:25:28 -0600 Subject: [PATCH] List assignments with names When a client calls list assignment API what is returned is the role id, user id or group id, and project id or domain id. Most users then call the api again for each of these entities to get their names, creating many api calls between the client and server. This can be reduced by having the server do all the work instead. This commit adds the functionality to include the user, role, group, project, and domain names with the response if the parameter 'include_names' is set to True. Change-Id: I0a1cc986b8a35aeafe567e5e7fee6eeb848ae113 Closes-Bug: #1479569 Implements: blueprint list-assignment-with-names --- keystone/assignment/controllers.py | 49 ++++++++-- keystone/assignment/core.py | 54 ++++++++++- keystone/tests/unit/test_backend.py | 74 +++++++++++++++ keystone/tests/unit/test_backend_ldap.py | 3 + keystone/tests/unit/test_v3.py | 61 ++++++++++++ keystone/tests/unit/test_v3_assignment.py | 92 +++++++++++++++++++ ...ole_assignment_names-33aedc1e521230b6.yaml | 6 ++ 7 files changed, 328 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/list_role_assignment_names-33aedc1e521230b6.yaml diff --git a/keystone/assignment/controllers.py b/keystone/assignment/controllers.py index be3fd5c905..ca19adcb43 100644 --- a/keystone/assignment/controllers.py +++ b/keystone/assignment/controllers.py @@ -514,8 +514,15 @@ class RoleAssignmentV3(controller.V3Controller): inherited_assignment = entity.get('inherited_to_projects') if 'project_id' in entity: - formatted_entity['scope'] = { - 'project': {'id': entity['project_id']}} + if 'project_name' in entity: + formatted_entity['scope'] = {'project': { + 'id': entity['project_id'], + 'name': entity['project_name'], + 'domain': {'id': entity['project_domain_id'], + 'name': entity['project_domain_name']}}} + else: + formatted_entity['scope'] = { + 'project': {'id': entity['project_id']}} if 'domain_id' in entity.get('indirect', {}): inherited_assignment = True @@ -528,12 +535,24 @@ class RoleAssignmentV3(controller.V3Controller): else: formatted_link = '/projects/%s' % entity['project_id'] elif 'domain_id' in entity: - formatted_entity['scope'] = {'domain': {'id': entity['domain_id']}} + if 'domain_name' in entity: + formatted_entity['scope'] = { + 'domain': {'id': entity['domain_id'], + 'name': entity['domain_name']}} + else: + formatted_entity['scope'] = { + 'domain': {'id': entity['domain_id']}} formatted_link = '/domains/%s' % entity['domain_id'] if 'user_id' in entity: - formatted_entity['user'] = {'id': entity['user_id']} - + if 'user_name' in entity: + formatted_entity['user'] = { + 'id': entity['user_id'], + 'name': entity['user_name'], + 'domain': {'id': entity['user_domain_id'], + 'name': entity['user_domain_name']}} + else: + formatted_entity['user'] = {'id': entity['user_id']} if 'group_id' in entity.get('indirect', {}): membership_url = ( self.base_url(context, '/groups/%s/users/%s' % ( @@ -543,10 +562,21 @@ class RoleAssignmentV3(controller.V3Controller): else: formatted_link += '/users/%s' % entity['user_id'] elif 'group_id' in entity: - formatted_entity['group'] = {'id': entity['group_id']} + if 'group_name' in entity: + formatted_entity['group'] = { + 'id': entity['group_id'], + 'name': entity['group_name'], + 'domain': {'id': entity['group_domain_id'], + 'name': entity['group_domain_name']}} + else: + formatted_entity['group'] = {'id': entity['group_id']} formatted_link += '/groups/%s' % entity['group_id'] - formatted_entity['role'] = {'id': entity['role_id']} + if 'role_name' in entity: + formatted_entity['role'] = {'id': entity['role_id'], + 'name': entity['role_name']} + else: + formatted_entity['role'] = {'id': entity['role_id']} formatted_link += '/roles/%s' % entity['role_id'] if inherited_assignment: @@ -616,6 +646,8 @@ class RoleAssignmentV3(controller.V3Controller): params = context['query_string'] effective = 'effective' in params and ( self.query_filter_is_true(params['effective'])) + include_names = ('include_names' in params and + self.query_filter_is_true(params['include_names'])) if 'scope.OS-INHERIT:inherited_to' in params: inherited = ( @@ -642,7 +674,8 @@ class RoleAssignmentV3(controller.V3Controller): domain_id=params.get('scope.domain.id'), project_id=params.get('scope.project.id'), include_subtree=include_subtree, - inherited=inherited, effective=effective) + inherited=inherited, effective=effective, + include_names=include_names) formatted_refs = [self._format_entity(context, ref) for ref in refs] diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py index e527d34670..65da3417fd 100644 --- a/keystone/assignment/core.py +++ b/keystone/assignment/core.py @@ -823,7 +823,7 @@ class Manager(manager.Manager): def list_role_assignments(self, role_id=None, user_id=None, group_id=None, domain_id=None, project_id=None, include_subtree=False, inherited=None, - effective=None): + effective=None, include_names=False): """List role assignments, honoring effective mode and provided filters. Returns a list of role assignments, where their attributes match the @@ -841,6 +841,9 @@ class Manager(manager.Manager): 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 include_names is set to true the entities' names are returned + in addition to their id's + If OS-INHERIT extension is disabled or the used driver does not support inherited roles retrieval, inherited role assignments will be ignored. @@ -857,14 +860,59 @@ class Manager(manager.Manager): self.resource_api.list_projects_in_subtree(project_id)]) if effective: - return self._list_effective_role_assignments( + role_assignments = self._list_effective_role_assignments( role_id, user_id, group_id, domain_id, project_id, subtree_ids, inherited) else: - return self._list_direct_role_assignments( + role_assignments = self._list_direct_role_assignments( role_id, user_id, group_id, domain_id, project_id, subtree_ids, inherited) + if include_names: + return self._get_names_from_role_assignments(role_assignments) + return role_assignments + + def _get_names_from_role_assignments(self, role_assignments): + role_assign_list = [] + + for role_asgmt in role_assignments: + new_assign = {} + for id_type, id_ in role_asgmt.items(): + if id_type == 'domain_id': + _domain = self.resource_api.get_domain(id_) + new_assign['domain_id'] = _domain['id'] + new_assign['domain_name'] = _domain['name'] + elif id_type == 'user_id': + _user = self.identity_api.get_user(id_) + new_assign['user_id'] = _user['id'] + new_assign['user_name'] = _user['name'] + new_assign['user_domain_id'] = _user['domain_id'] + new_assign['user_domain_name'] = ( + self.resource_api.get_domain(_user['domain_id']) + ['name']) + elif id_type == 'group_id': + _group = self.identity_api.get_group(id_) + new_assign['group_id'] = _group['id'] + new_assign['group_name'] = _group['name'] + new_assign['group_domain_id'] = _group['domain_id'] + new_assign['group_domain_name'] = ( + self.resource_api.get_domain(_group['domain_id']) + ['name']) + elif id_type == 'project_id': + _project = self.resource_api.get_project(id_) + new_assign['project_id'] = _project['id'] + new_assign['project_name'] = _project['name'] + new_assign['project_domain_id'] = _project['domain_id'] + new_assign['project_domain_name'] = ( + self.resource_api.get_domain(_project['domain_id']) + ['name']) + elif id_type == 'role_id': + _role = self.role_api.get_role(id_) + new_assign['role_id'] = _role['id'] + new_assign['role_name'] = _role['name'] + role_assign_list.append(new_assign) + return role_assign_list + def delete_tokens_for_role_assignments(self, role_id): assignments = self.list_role_assignments(role_id=role_id) diff --git a/keystone/tests/unit/test_backend.py b/keystone/tests/unit/test_backend.py index feb205d520..aed80fa0d5 100644 --- a/keystone/tests/unit/test_backend.py +++ b/keystone/tests/unit/test_backend.py @@ -4127,6 +4127,80 @@ class IdentityTests(AssignmentTestHelperMixin): {'name': self.role_member['name']}) # If the previous line didn't raise an exception then the test passes. + def test_list_role_assignment_containing_names(self): + # Create Refs + new_role = unit.new_role_ref() + new_domain = self._get_domain_fixture() + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_project = unit.new_project_ref(domain_id=new_domain['id']) + new_group = unit.new_group_ref(domain_id=new_domain['id']) + # Create entities + new_role = self.role_api.create_role(new_role['id'], new_role) + new_user = self.identity_api.create_user(new_user) + new_group = self.identity_api.create_group(new_group) + self.resource_api.create_project(new_project['id'], new_project) + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id=new_role['id']) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id=new_role['id']) + self.assignment_api.create_grant(domain_id=new_domain['id'], + user_id=new_user['id'], + role_id=new_role['id']) + # Get the created assignments with the include_names flag + _asgmt_prj = self.assignment_api.list_role_assignments( + user_id=new_user['id'], + project_id=new_project['id'], + include_names=True) + _asgmt_grp = self.assignment_api.list_role_assignments( + group_id=new_group['id'], + project_id=new_project['id'], + include_names=True) + _asgmt_dmn = self.assignment_api.list_role_assignments( + domain_id=new_domain['id'], + user_id=new_user['id'], + include_names=True) + # Make sure we can get back the correct number of assignments + self.assertThat(_asgmt_prj, matchers.HasLength(1)) + self.assertThat(_asgmt_grp, matchers.HasLength(1)) + self.assertThat(_asgmt_dmn, matchers.HasLength(1)) + # get the first assignment + first_asgmt_prj = _asgmt_prj[0] + first_asgmt_grp = _asgmt_grp[0] + first_asgmt_dmn = _asgmt_dmn[0] + # Assert the names are correct in the project response + self.assertEqual(new_project['name'], + first_asgmt_prj['project_name']) + self.assertEqual(new_project['domain_id'], + first_asgmt_prj['project_domain_id']) + self.assertEqual(new_user['name'], + first_asgmt_prj['user_name']) + self.assertEqual(new_user['domain_id'], + first_asgmt_prj['user_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_prj['role_name']) + # Assert the names are correct in the group response + self.assertEqual(new_group['name'], + first_asgmt_grp['group_name']) + self.assertEqual(new_group['domain_id'], + first_asgmt_grp['group_domain_id']) + self.assertEqual(new_project['name'], + first_asgmt_grp['project_name']) + self.assertEqual(new_project['domain_id'], + first_asgmt_grp['project_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_grp['role_name']) + # Assert the names are correct in the domain response + self.assertEqual(new_domain['name'], + first_asgmt_dmn['domain_name']) + self.assertEqual(new_user['name'], + first_asgmt_dmn['user_name']) + self.assertEqual(new_user['domain_id'], + first_asgmt_dmn['user_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_dmn['role_name']) + class TokenTests(object): def _create_token_id(self): diff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py index 5ef3bfcd80..633436616a 100644 --- a/keystone/tests/unit/test_backend_ldap.py +++ b/keystone/tests/unit/test_backend_ldap.py @@ -329,6 +329,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_delete_group_with_user_project_domain_links(self): self.skipTest('N/A: LDAP does not support multiple domains') + def test_list_role_assignment_containing_names(self): + self.skipTest('N/A: LDAP does not support multiple domains') + def test_list_projects_for_user(self): domain = self._get_domain_fixture() user1 = self.new_user_ref(domain_id=domain['id']) diff --git a/keystone/tests/unit/test_v3.py b/keystone/tests/unit/test_v3.py index 526aec221d..c4e790416c 100644 --- a/keystone/tests/unit/test_v3.py +++ b/keystone/tests/unit/test_v3.py @@ -1321,3 +1321,64 @@ class AssignmentTestMixin(object): entity['scope']['OS-INHERIT:inherited_to'] = 'projects' return entity + + def build_role_assignment_entity_include_names(self, + domain_ref=None, + role_ref=None, + group_ref=None, + user_ref=None, + project_ref=None, + inherited_assignment=None): + """Build and return a role assignment entity with provided attributes. + + The expected attributes are: domain_ref or project_ref, + user_ref or group_ref, role_ref and, optionally, inherited_to_projects. + """ + entity = {'links': {}} + attributes_for_links = {} + if project_ref: + dmn_name = self.resource_api.get_domain( + project_ref['domain_id'])['name'] + + entity['scope'] = {'project': { + 'id': project_ref['id'], + 'name': project_ref['name'], + 'domain': { + 'id': project_ref['domain_id'], + 'name': dmn_name}}} + attributes_for_links['project_id'] = project_ref['id'] + else: + entity['scope'] = {'domain': {'id': domain_ref['id'], + 'name': domain_ref['name']}} + attributes_for_links['domain_id'] = domain_ref['id'] + if user_ref: + dmn_name = self.resource_api.get_domain( + user_ref['domain_id'])['name'] + entity['user'] = {'id': user_ref['id'], + 'name': user_ref['name'], + 'domain': {'id': user_ref['domain_id'], + 'name': dmn_name}} + attributes_for_links['user_id'] = user_ref['id'] + else: + dmn_name = self.resource_api.get_domain( + group_ref['domain_id'])['name'] + entity['group'] = {'id': group_ref['id'], + 'name': group_ref['name'], + 'domain': { + 'id': group_ref['domain_id'], + 'name': dmn_name}} + attributes_for_links['group_id'] = group_ref['id'] + + if role_ref: + entity['role'] = {'id': role_ref['id'], + 'name': role_ref['name']} + attributes_for_links['role_id'] = role_ref['id'] + + if inherited_assignment: + entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + attributes_for_links['inherited_to_projects'] = True + + entity['links']['assignment'] = self.build_role_assignment_link( + **attributes_for_links) + + return entity diff --git a/keystone/tests/unit/test_v3_assignment.py b/keystone/tests/unit/test_v3_assignment.py index 73e45be299..c320e6db01 100644 --- a/keystone/tests/unit/test_v3_assignment.py +++ b/keystone/tests/unit/test_v3_assignment.py @@ -1429,6 +1429,98 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, up_entity) + def test_list_role_assignments_include_names(self): + """Call ``GET /role_assignments with include names``. + + Test Plan: + + - Create a domain with a group and a user + - Create a project with a group and a user + + """ + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + user1 = unit.create_user(self.identity_api, domain_id=self.domain_id) + group = unit.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group) + project1 = unit.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(project1['id'], project1) + + expected_entity1 = self.build_role_assignment_entity_include_names( + role_ref=role1, + project_ref=project1, + user_ref=user1) + self.put(expected_entity1['links']['assignment']) + expected_entity2 = self.build_role_assignment_entity_include_names( + role_ref=role1, + domain_ref=self.domain, + group_ref=group) + self.put(expected_entity2['links']['assignment']) + expected_entity3 = self.build_role_assignment_entity_include_names( + role_ref=role1, + domain_ref=self.domain, + user_ref=user1) + self.put(expected_entity3['links']['assignment']) + expected_entity4 = self.build_role_assignment_entity_include_names( + role_ref=role1, + project_ref=project1, + group_ref=group) + self.put(expected_entity4['links']['assignment']) + + collection_url_domain = ( + '/role_assignments?include_names&scope.domain.id=%(domain_id)s' % { + 'domain_id': self.domain_id}) + rs_domain = self.get(collection_url_domain) + collection_url_project = ( + '/role_assignments?include_names&' + 'scope.project.id=%(project_id)s' % { + 'project_id': project1['id']}) + rs_project = self.get(collection_url_project) + collection_url_group = ( + '/role_assignments?include_names&group.id=%(group_id)s' % { + 'group_id': group['id']}) + rs_group = self.get(collection_url_group) + collection_url_user = ( + '/role_assignments?include_names&user.id=%(user_id)s' % { + 'user_id': user1['id']}) + rs_user = self.get(collection_url_user) + collection_url_role = ( + '/role_assignments?include_names&role.id=%(role_id)s' % { + 'role_id': role1['id']}) + rs_role = self.get(collection_url_role) + # Make sure all entities were created successfully + self.assertEqual(rs_domain.status_int, http_client.OK) + self.assertEqual(rs_project.status_int, http_client.OK) + self.assertEqual(rs_group.status_int, http_client.OK) + self.assertEqual(rs_user.status_int, http_client.OK) + # Make sure we can get back the correct number of entities + self.assertValidRoleAssignmentListResponse( + rs_domain, + expected_length=2, + resource_url=collection_url_domain) + self.assertValidRoleAssignmentListResponse( + rs_project, + expected_length=2, + resource_url=collection_url_project) + self.assertValidRoleAssignmentListResponse( + rs_group, + expected_length=2, + resource_url=collection_url_group) + self.assertValidRoleAssignmentListResponse( + rs_user, + expected_length=2, + resource_url=collection_url_user) + self.assertValidRoleAssignmentListResponse( + rs_role, + expected_length=4, + resource_url=collection_url_role) + # Verify all types of entities have the correct format + self.assertRoleAssignmentInListResponse(rs_domain, expected_entity2) + self.assertRoleAssignmentInListResponse(rs_project, expected_entity1) + self.assertRoleAssignmentInListResponse(rs_group, expected_entity4) + self.assertRoleAssignmentInListResponse(rs_user, expected_entity3) + self.assertRoleAssignmentInListResponse(rs_role, expected_entity1) + def test_list_role_assignments_for_disabled_inheritance_extension(self): """Call ``GET /role_assignments with inherited domain grants``. diff --git a/releasenotes/notes/list_role_assignment_names-33aedc1e521230b6.yaml b/releasenotes/notes/list_role_assignment_names-33aedc1e521230b6.yaml new file mode 100644 index 0000000000..fd1fe665ce --- /dev/null +++ b/releasenotes/notes/list_role_assignment_names-33aedc1e521230b6.yaml @@ -0,0 +1,6 @@ +--- +features: + - > + [`bug 1479569 `_] + Names have been added to list role assignments, rather than returning + just the internal ids of the objects the names are also returned. \ No newline at end of file