diff --git a/doc/source/policy_mapping.rst b/doc/source/policy_mapping.rst index 238934ed44..c67c322234 100644 --- a/doc/source/policy_mapping.rst +++ b/doc/source/policy_mapping.rst @@ -73,6 +73,13 @@ identity:create_role POST /v3/roles identity:update_role PATCH /v3/roles/{role_id} identity:delete_role DELETE /v3/roles/{role_id} +identity:get_implied_role GET /v3/roles/{prior_role_id}/implies/{implied_role_id} +identity:list_implied_roles GET /v3/roles/{prior_role_id}/implies +identity:create_implied_role PUT /v3/roles/{prior_role_id}/implies/{implied_role_id} +identity:delete_implied_role DELETE /v3/roles/{prior_role_id}/implies/{implied_role_id} +identity:list_role_inference_rules GET /v3/role_inferences +identity:check_implied_role HEAD /v3/roles/{prior_role_id}/implies/{implied_role_id} + identity:check_grant GET `grant_resources`_ identity:list_grants GET `grant_collections`_ identity:create_grant PUT `grant_resources`_ diff --git a/etc/policy.json b/etc/policy.json index 47aa9efd81..bab0938e96 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -76,6 +76,13 @@ "identity:update_role": "rule:admin_required", "identity:delete_role": "rule:admin_required", + "identity:get_implied_role": "rule:admin_required ", + "identity:list_implied_roles": "rule:admin_required", + "identity:create_implied_role": "rule:admin_required", + "identity:delete_implied_role": "rule:admin_required", + "identity:list_role_inference_rules": "rule:admin_required", + "identity:check_implied_role": "rule:admin_required", + "identity:check_grant": "rule:admin_required", "identity:list_grants": "rule:admin_required", "identity:create_grant": "rule:admin_required", diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json index 47b5b90c84..3cb5f1c398 100644 --- a/etc/policy.v3cloudsample.json +++ b/etc/policy.v3cloudsample.json @@ -82,6 +82,13 @@ "identity:update_role": "rule:cloud_admin", "identity:delete_role": "rule:cloud_admin", + "identity:get_implied_role": "rule:cloud_admin", + "identity:list_implied_roles": "rule:cloud_admin", + "identity:create_implied_role": "rule:cloud_admin", + "identity:delete_implied_role": "rule:cloud_admin", + "identity:list_role_inference_rules": "rule:cloud_admin", + "identity:check_implied_role": "rule:cloud_admin", + "domain_admin_for_grants": "rule:admin_required and (domain_id:%(domain_id)s or domain_id:%(target.project.domain_id)s)", "project_admin_for_grants": "rule:admin_required and project_id:%(project_id)s", "identity:check_grant": "rule:cloud_admin or rule:domain_admin_for_grants or rule:project_admin_for_grants", diff --git a/keystone/assignment/controllers.py b/keystone/assignment/controllers.py index 1c9bbcbcdf..6e532224ca 100644 --- a/keystone/assignment/controllers.py +++ b/keystone/assignment/controllers.py @@ -27,6 +27,7 @@ from keystone.common import controller from keystone.common import dependency from keystone.common import utils from keystone.common import validation +from keystone.common import wsgi from keystone import exception from keystone.i18n import _ from keystone import notifications @@ -328,6 +329,125 @@ class RoleV3(controller.V3Controller): self.role_api.delete_role(role_id, initiator) +@dependency.requires('role_api') +class ImpliedRolesV3(controller.V3Controller): + """The V3 ImpliedRoles CRD APIs. There is no Update.""" + + def _prior_role_stanza(self, endpoint, prior_role_id, prior_role_name): + return { + "id": prior_role_id, + "links": { + "self": endpoint + "/v3/roles/" + prior_role_id + }, + "name": prior_role_name + } + + def _implied_role_stanza(self, endpoint, implied_role): + implied_id = implied_role['id'] + implied_response = { + "id": implied_id, + "links": { + "self": endpoint + "/v3/roles/" + implied_id + }, + "name": implied_role['name'] + } + return implied_response + + def _populate_prior_role_response(self, endpoint, prior_id): + prior_role = self.role_api.get_role(prior_id) + response = { + "role_inference": { + "prior_role": self._prior_role_stanza( + endpoint, prior_id, prior_role['name']) + } + } + return response + + def _populate_implied_roles_response(self, endpoint, + prior_id, implied_ids): + response = self._populate_prior_role_response(endpoint, prior_id) + response["role_inference"]['implies'] = [] + for implied_id in implied_ids: + implied_role = self.role_api.get_role(implied_id) + implied_response = self._implied_role_stanza( + endpoint, implied_role) + response["role_inference"]['implies'].append(implied_response) + return response + + def _populate_implied_role_response(self, endpoint, prior_id, implied_id): + response = self._populate_prior_role_response(endpoint, prior_id) + implied_role = self.role_api.get_role(implied_id) + stanza = self._implied_role_stanza(endpoint, implied_role) + response["role_inference"]['implies'] = stanza + return response + + @controller.protected() + def get_implied_role(self, context, prior_role_id, implied_role_id): + ref = self.role_api.get_implied_role(prior_role_id, implied_role_id) + + prior_id = ref['prior_role_id'] + implied_id = ref['implied_role_id'] + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + response = self._populate_implied_role_response( + endpoint, prior_id, implied_id) + return response + + @controller.protected() + def check_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.get_implied_role(prior_role_id, implied_role_id) + + @controller.protected() + def create_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.create_implied_role(prior_role_id, implied_role_id) + return wsgi.render_response( + self.get_implied_role(context, prior_role_id, implied_role_id), + status=(201, 'Created')) + + @controller.protected() + def delete_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.delete_implied_role(prior_role_id, implied_role_id) + + @controller.protected() + def list_implied_roles(self, context, prior_role_id): + ref = self.role_api.list_implied_roles(prior_role_id) + implied_ids = [r['implied_role_id'] for r in ref] + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + + results = self._populate_implied_roles_response( + endpoint, prior_role_id, implied_ids) + + return results + + @controller.protected() + def list_role_inference_rules(self, context): + refs = self.role_api.list_role_inference_rules() + role_dict = {role_ref['id']: role_ref + for role_ref in self.role_api.list_roles()} + + rules = dict() + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + + for ref in refs: + implied_role_id = ref['implied_role_id'] + prior_role_id = ref['prior_role_id'] + implied = rules.get(prior_role_id, []) + implied.append(self._implied_role_stanza( + endpoint, role_dict[implied_role_id])) + rules[prior_role_id] = implied + + inferences = [] + for prior_id, implied in rules.items(): + prior_response = self._prior_role_stanza( + endpoint, prior_id, role_dict[prior_id]['name']) + inferences.append({'prior_role': prior_response, + 'implies': implied}) + results = {'role_inferences': inferences} + return results + + @dependency.requires('assignment_api', 'identity_api', 'resource_api', 'role_api') class GrantAssignmentV3(controller.V3Controller): @@ -480,6 +600,13 @@ class RoleAssignmentV3(controller.V3Controller): 'role_id': role_id, 'indirect': {'project_id': parent_id}} + or, for a role that was implied by a prior role: + + {'user_id': user_id, + 'project_id': project_id, + 'role_id': role_id, + 'indirect': {'role_id': prior role_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 @@ -577,7 +704,16 @@ class RoleAssignmentV3(controller.V3Controller): 'name': entity['role_name']} else: formatted_entity['role'] = {'id': entity['role_id']} - formatted_link += '/roles/%s' % entity['role_id'] + prior_role_link = '' + if 'role_id' in entity.get('indirect', {}): + formatted_link += '/roles/%s' % entity['indirect']['role_id'] + prior_role_link = ( + '/prior_role/%(prior)s/implies/%(implied)s' % { + 'prior': entity['role_id'], + 'implied': entity['indirect']['role_id'] + }) + else: + formatted_link += '/roles/%s' % entity['role_id'] if inherited_assignment: formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( @@ -587,6 +723,9 @@ class RoleAssignmentV3(controller.V3Controller): formatted_entity['links']['assignment'] = self.base_url(context, formatted_link) + if prior_role_link: + formatted_entity['links']['prior_role'] = ( + self.base_url(context, prior_role_link)) return formatted_entity diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py index a72d3b3c69..1d6afe5b26 100644 --- a/keystone/assignment/core.py +++ b/keystone/assignment/core.py @@ -1452,6 +1452,21 @@ class RoleManager(manager.Manager): self.get_role.invalidate(self, role_id) COMPUTED_ASSIGNMENTS_REGION.invalidate() + # TODO(ayoung): Add notification + def create_implied_role(self, prior_role_id, implied_role_id): + implied_role = self.driver.get_role(implied_role_id) + self.driver.get_role(prior_role_id) + if implied_role['name'] == CONF.assignment.root_role: + raise exception.InvalidImpliedRole(role_id=implied_role_id) + response = self.driver.create_implied_role( + prior_role_id, implied_role_id) + COMPUTED_ASSIGNMENTS_REGION.invalidate() + return response + + def delete_implied_role(self, prior_role_id, implied_role_id): + self.driver.delete_implied_role(prior_role_id, implied_role_id) + COMPUTED_ASSIGNMENTS_REGION.invalidate() + # The RoleDriverBase class is the set of driver methods from earlier # drivers that we still support, that have not been removed or modified. This diff --git a/keystone/assignment/routers.py b/keystone/assignment/routers.py index f3ebb0a75e..f2eef64776 100644 --- a/keystone/assignment/routers.py +++ b/keystone/assignment/routers.py @@ -73,6 +73,41 @@ class Routers(wsgi.RoutersBase): router.Router(controllers.RoleV3(), 'roles', 'role', resource_descriptions=self.v3_resources)) + implied_roles_controller = controllers.ImpliedRolesV3() + self._add_resource( + mapper, implied_roles_controller, + path='/roles/{prior_role_id}/implies', + rel=json_home.build_v3_resource_relation('implied_roles'), + get_action='list_implied_roles', + status=json_home.Status.EXPERIMENTAL, + path_vars={ + 'prior_role_id': json_home.Parameters.ROLE_ID, + } + ) + + self._add_resource( + mapper, implied_roles_controller, + path='/roles/{prior_role_id}/implies/{implied_role_id}', + put_action='create_implied_role', + delete_action='delete_implied_role', + head_action='check_implied_role', + get_action='get_implied_role', + rel=json_home.build_v3_resource_relation('implied_role'), + status=json_home.Status.EXPERIMENTAL, + path_vars={ + 'prior_role_id': json_home.Parameters.ROLE_ID, + 'implied_role_id': json_home.Parameters.ROLE_ID + } + ) + self._add_resource( + mapper, implied_roles_controller, + path='/role_inferences', + get_action='list_role_inference_rules', + rel=json_home.build_v3_resource_relation('role_inferences'), + status=json_home.Status.EXPERIMENTAL, + path_vars={} + ) + grant_controller = controllers.GrantAssignmentV3() self._add_resource( mapper, grant_controller, diff --git a/keystone/common/config.py b/keystone/common/config.py index c907ac069a..792042b465 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -378,6 +378,11 @@ FILE_OPTIONS = { 'keystone.assignment namespace. Only an SQL driver is ' 'supplied.', default='sql'), + cfg.StrOpt('root_role', default='admin', + help='A role that is not allowed to be an implied ' + 'role, as it is the root of role inference directed ' + 'acyclic graph.'), + ], 'resource': [ cfg.StrOpt('driver', diff --git a/keystone/exception.py b/keystone/exception.py index b31c2a5418..c97231442f 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -289,6 +289,10 @@ class ImpliedRoleNotFound(NotFound): message_format = _("%(prior_role_id)s does not imply %(implied_role_id)s") +class InvalidImpliedRole(Forbidden): + message_format = _("%(role_id)s cannot be an implied roles") + + class RoleAssignmentNotFound(NotFound): message_format = _("Could not find role assignment with role: " "%(role_id)s, user or group: %(actor_id)s, " diff --git a/keystone/tests/unit/test_v3.py b/keystone/tests/unit/test_v3.py index 6427806a6c..bf6da547e0 100644 --- a/keystone/tests/unit/test_v3.py +++ b/keystone/tests/unit/test_v3.py @@ -1366,7 +1366,8 @@ class AssignmentTestMixin(object): return link - def build_role_assignment_entity(self, link=None, **attribs): + def build_role_assignment_entity( + self, link=None, prior_role_link=None, **attribs): """Build and return a role assignment entity with provided attributes. Provided attributes are expected to contain: domain_id or project_id, @@ -1395,6 +1396,9 @@ class AssignmentTestMixin(object): if attribs.get('inherited_to_projects'): entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + if prior_role_link: + entity['links']['prior_role'] = prior_role_link + return entity def build_role_assignment_entity_include_names(self, diff --git a/keystone/tests/unit/test_v3_assignment.py b/keystone/tests/unit/test_v3_assignment.py index fc416d2b02..5ad813f8c4 100644 --- a/keystone/tests/unit/test_v3_assignment.py +++ b/keystone/tests/unit/test_v3_assignment.py @@ -2386,3 +2386,194 @@ class AssignmentInheritanceDisabledTestCase(test_v3.RestfulTestCase): self.head(member_url, expected_status=http_client.NOT_FOUND) self.get(collection_url, expected_status=http_client.NOT_FOUND) self.delete(member_url, expected_status=http_client.NOT_FOUND) + + +class ImpliedRolesTests(test_v3.RestfulTestCase, test_v3.AssignmentTestMixin, + unit.TestCase): + def _create_role(self): + """Call ``POST /roles``.""" + ref = unit.new_role_ref() + r = self.post('/roles', body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + def test_list_implied_roles_none(self): + self.prior = self._create_role() + url = '/roles/%s/implies' % (self.prior['id']) + response = self.get(url).json["role_inference"] + self.assertEqual(self.prior['id'], response['prior_role']['id']) + self.assertEqual(0, len(response['implies'])) + + def _create_implied_role(self, prior, implied): + self.put('/roles/%s/implies/%s' % (prior['id'], implied['id']), + expected_status=http_client.CREATED) + + def _delete_implied_role(self, prior, implied): + self.delete('/roles/%s/implies/%s' % (prior['id'], implied['id'])) + + def _setup_prior_two_implied(self): + self.prior = self._create_role() + self.implied1 = self._create_role() + self._create_implied_role(self.prior, self.implied1) + self.implied2 = self._create_role() + self._create_implied_role(self.prior, self.implied2) + + def _assert_expected_implied_role_response( + self, expected_prior_id, expected_implied_ids): + r = self.get('/roles/%s/implies' % expected_prior_id) + response = r.json["role_inference"] + self.assertEqual(expected_prior_id, response['prior_role']['id']) + + actual_implied_ids = [implied['id'] for implied in response['implies']] + + for expected_id in expected_implied_ids: + self.assertIn(expected_id, actual_implied_ids) + self.assertEqual(len(expected_implied_ids), len(response['implies'])) + + self.assertIsNotNone(response['prior_role']['links']['self']) + for implied in response['implies']: + self.assertIsNotNone(implied['links']['self']) + + def _assert_two_roles_implied(self): + self._assert_expected_implied_role_response( + self.prior['id'], [self.implied1['id'], self.implied2['id']]) + + def _assert_one_role_implied(self): + self._assert_expected_implied_role_response( + self.prior['id'], [self.implied1['id']]) + + self.get('/roles/%s/implies/%s' % + (self.prior['id'], self.implied2['id']), + expected_status=http_client.NOT_FOUND) + + def _assert_two_rules_defined(self): + r = self.get('/role_inferences/') + + rules = r.result['role_inferences'] + + self.assertEqual(self.prior['id'], rules[0]['prior_role']['id']) + self.assertEqual(2, len(rules[0]['implies'])) + implied_ids = [implied['id'] for implied in rules[0]['implies']] + implied_names = [implied['name'] for implied in rules[0]['implies']] + + self.assertIn(self.implied1['id'], implied_ids) + self.assertIn(self.implied2['id'], implied_ids) + self.assertIn(self.implied1['name'], implied_names) + self.assertIn(self.implied2['name'], implied_names) + + def _assert_one_rule_defined(self): + r = self.get('/role_inferences/') + rules = r.result['role_inferences'] + self.assertEqual(self.prior['id'], rules[0]['prior_role']['id']) + self.assertEqual(self.implied1['id'], rules[0]['implies'][0]['id']) + self.assertEqual(self.implied1['name'], rules[0]['implies'][0]['name']) + self.assertEqual(1, len(rules[0]['implies'])) + + def test_list_all_rules(self): + self._setup_prior_two_implied() + self._assert_two_rules_defined() + + self._delete_implied_role(self.prior, self.implied2) + self._assert_one_rule_defined() + + def test_CRD_implied_roles(self): + + self._setup_prior_two_implied() + self._assert_two_roles_implied() + + self._delete_implied_role(self.prior, self.implied2) + self._assert_one_role_implied() + + def _create_three_roles(self): + self.role_list = [] + for _ in range(3): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + self.role_list.append(role) + + def _create_test_domain_user_project(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user = unit.create_user(self.identity_api, domain_id=domain['id']) + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + return domain, user, project + + def _assign_top_role_to_user_on_project(self, user, project): + self.assignment_api.add_role_to_user_and_project( + user['id'], project['id'], self.role_list[0]['id']) + + def _build_effective_role_assignments_url(self, user): + return '/role_assignments?effective&user.id=%(user_id)s' % { + 'user_id': user['id']} + + def _assert_all_roles_in_assignment(self, response, user): + # Now use the list role assignments api to check that all three roles + # appear in the collection + self.assertValidRoleAssignmentListResponse( + response, + expected_length=len(self.role_list), + resource_url=self._build_effective_role_assignments_url(user)) + + def _assert_initial_assignment_in_effective(self, response, user, project): + # The initial assignment should be there (the link url will be + # generated and checked automatically since it matches the assignment) + entity = self.build_role_assignment_entity( + project_id=project['id'], + user_id=user['id'], role_id=self.role_list[0]['id']) + self.assertRoleAssignmentInListResponse(response, entity) + + def _assert_effective_role_for_implied_has_prior_in_links( + self, response, user, project, prior_index, implied_index): + # An effective role for an implied role will have the prior role + # assignment in the links + prior_link = '/prior_roles/%(prior)s/implies/%(implied)s' % { + 'prior': self.role_list[prior_index]['id'], + 'implied': self.role_list[implied_index]['id']} + link = self.build_role_assignment_link( + project_id=project['id'], user_id=user['id'], + role_id=self.role_list[prior_index]['id']) + entity = self.build_role_assignment_entity( + link=link, project_id=project['id'], + user_id=user['id'], role_id=self.role_list[implied_index]['id'], + prior_link=prior_link) + self.assertRoleAssignmentInListResponse(response, entity) + + def test_list_role_assignments_with_implied_roles(self): + """Call ``GET /role_assignments`` with implied role grant. + + Test Plan: + - Create a domain with a user and a project + - Create 3 roles + - Role 0 implies role 1 and role 1 implies role 2 + - Assign the top role to the project + - Issue the URL to check effective roles on project - this + should return all 3 roles. + - Check the links of the 3 roles indicate the prior role where + appropriate + + """ + (domain, user, project) = self._create_test_domain_user_project() + self._create_three_roles() + self._create_implied_role(self.role_list[0], self.role_list[1]) + self._create_implied_role(self.role_list[1], self.role_list[2]) + self._assign_top_role_to_user_on_project(user, project) + + response = self.get(self._build_effective_role_assignments_url(user)) + r = response + + self._assert_all_roles_in_assignment(r, user) + self._assert_initial_assignment_in_effective(response, user, project) + self._assert_effective_role_for_implied_has_prior_in_links( + response, user, project, 0, 1) + self._assert_effective_role_for_implied_has_prior_in_links( + response, user, project, 1, 2) + + def test_root_role_as_implied_role_forbidden(self): + self.config_fixture.config(group='assignment', root_role='root') + + root_role = unit.new_role_ref() + root_role['name'] = 'root' + self.role_api.create_role(root_role['id'], root_role) + prior = self._create_role() + url = '/roles/%s/implies/%s' % (prior['id'], root_role['id']) + self.put(url, expected_status=http_client.FORBIDDEN) diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py index ced2676f7b..d7feb35a1a 100644 --- a/keystone/tests/unit/test_v3_auth.py +++ b/keystone/tests/unit/test_v3_auth.py @@ -481,6 +481,191 @@ class TokenAPITests(object): r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + def _create_role(self): + """Call ``POST /roles``.""" + ref = unit.new_role_ref() + r = self.post('/roles', body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + def _create_implied_role(self, prior_id): + implied = self._create_role() + url = '/roles/%s/implies/%s' % (prior_id, implied['id']) + self.put(url, expected_status=http_client.CREATED) + return implied + + def _delete_implied_role(self, prior_role_id, implied_role_id): + url = '/roles/%s/implies/%s' % (prior_role_id, implied_role_id) + self.delete(url) + + def _get_scoped_token_roles(self, is_domain=False): + if is_domain: + v3_token = self.get_domain_scoped_token() + else: + v3_token = self.get_scoped_token() + + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + v3_token_data = r.result + token_roles = v3_token_data['token']['roles'] + return token_roles + + def _create_implied_role_shows_in_v3_token(self, is_domain): + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(2, len(token_roles)) + + implied2 = self._create_implied_role(prior) + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(3, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + + def test_create_implied_role_shows_in_v3_project_token(self): + # regardless of the default chosen, this should always + # test with the option set. + self.config_fixture.config(group='token', infer_roles=True) + self._create_implied_role_shows_in_v3_token(False) + + def test_create_implied_role_shows_in_v3_domain_token(self): + self.config_fixture.config(group='token', infer_roles=True) + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + + self._create_implied_role_shows_in_v3_token(True) + + def test_group_assigned_implied_role_shows_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + is_domain = False + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + new_role = self._create_role() + prior = new_role['id'] + + new_group_ref = unit.new_group_ref(domain_id=self.domain['id']) + new_group = self.identity_api.create_group(new_group_ref) + self.assignment_api.create_grant(prior, + group_id=new_group['id'], + project_id=self.project['id']) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + self.identity_api.add_user_to_group(self.user['id'], + new_group['id']) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(2, len(token_roles)) + + implied1 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(3, len(token_roles)) + + implied2 = self._create_implied_role(prior) + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + + def test_multiple_implied_roles_show_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(prior) + implied3 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + self.assertIn(implied3['id'], token_role_ids) + + def test_chained_implied_role_shows_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(implied1['id']) + implied3 = self._create_implied_role(implied2['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + self.assertIn(implied3['id'], token_role_ids) + + def test_implied_role_disabled_by_config(self): + self.config_fixture.config(group='token', infer_roles=False) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(implied1['id']) + self._create_implied_role(implied2['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + + def test_delete_implied_role_do_not_show_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + prior = token_roles[0]['id'] + implied = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + self._delete_implied_role(prior, implied['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + def test_unrelated_implied_roles_do_not_change_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + prior = token_roles[0]['id'] + implied = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + + unrelated = self._create_role() + url = '/roles/%s/implies/%s' % (unrelated['id'], implied['id']) + self.put(url, expected_status=http_client.CREATED) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + + self._delete_implied_role(unrelated['id'], implied['id']) + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + class TokenDataTests(object): """Test the data in specific token types.""" diff --git a/keystone/tests/unit/test_versions.py b/keystone/tests/unit/test_versions.py index b8ed41606f..0871880094 100644 --- a/keystone/tests/unit/test_versions.py +++ b/keystone/tests/unit/test_versions.py @@ -327,6 +327,22 @@ V3_JSON_HOME_RESOURCES = { 'href-template': '/roles/{role_id}', 'href-vars': { 'role_id': json_home.Parameters.ROLE_ID, }}, + json_home.build_v3_resource_relation('implied_roles'): { + 'href-template': '/roles/{prior_role_id}/implies', + 'href-vars': { + 'prior_role_id': json_home.Parameters.ROLE_ID}, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('implied_role'): { + 'href-template': + '/roles/{prior_role_id}/implies/{implied_role_id}', + 'href-vars': { + 'prior_role_id': json_home.Parameters.ROLE_ID, + 'implied_role_id': json_home.Parameters.ROLE_ID, + }, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('role_inferences'): { + 'href': '/role_inferences', + 'hints': {'status': 'experimental'}}, json_home.build_v3_resource_relation('role_assignments'): { 'href': '/role_assignments'}, json_home.build_v3_resource_relation('roles'): {'href': '/roles'},