diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json index dc606809f1..4ec1aa95e8 100644 --- a/etc/policy.v3cloudsample.json +++ b/etc/policy.v3cloudsample.json @@ -101,13 +101,20 @@ "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", - "identity:list_grants": "rule:cloud_admin or rule:domain_admin_for_grants or rule:project_admin_for_grants", + "identity:list_grants": "rule:cloud_admin or rule:domain_admin_for_list_grants or rule:project_admin_for_list_grants", "identity:create_grant": "rule:cloud_admin or rule:domain_admin_for_grants or rule:project_admin_for_grants", "identity:revoke_grant": "rule:cloud_admin or rule:domain_admin_for_grants or rule:project_admin_for_grants", - + "domain_admin_for_grants": "rule:domain_admin_for_global_role_grants or rule:domain_admin_for_domain_role_grants", + "domain_admin_for_global_role_grants": "rule:admin_required and None:%(target.role.domain_id)s and rule:domain_admin_grant_match", + "domain_admin_for_domain_role_grants": "rule:admin_required and domain_id:%(target.role.domain_id)s and rule:domain_admin_grant_match", + "domain_admin_grant_match": "domain_id:%(domain_id)s or domain_id:%(target.project.domain_id)s", + "project_admin_for_grants": "rule:project_admin_for_global_role_grants or rule:project_admin_for_domain_role_grants", + "project_admin_for_global_role_grants": "rule:admin_required and None:%(target.role.domain_id)s and project_id:%(project_id)s", + "project_admin_for_domain_role_grants": "rule:admin_required and project_domain_id:%(target.role.domain_id)s and project_id:%(project_id)s", + "domain_admin_for_list_grants": "rule:admin_required and rule:domain_admin_grant_match", + "project_admin_for_list_grants": "rule:admin_required and project_id:%(project_id)s", + "admin_on_domain_filter" : "rule:admin_required and domain_id:%(scope.domain.id)s", "admin_on_project_filter" : "rule:admin_required and project_id:%(scope.project.id)s", "admin_on_domain_of_project_filter" : "rule:admin_required and domain_id:%(target.project.domain_id)s", diff --git a/keystone/tests/unit/test_v3_protection.py b/keystone/tests/unit/test_v3_protection.py index aa325d27ad..6fe1ea9a88 100644 --- a/keystone/tests/unit/test_v3_protection.py +++ b/keystone/tests/unit/test_v3_protection.py @@ -735,9 +735,10 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.post('/domains', auth=self.auth, body={'domain': domain_ref}, expected_status=status_created) - def _test_grants(self, target, entity_id, expected=None): + def _test_grants(self, target, entity_id, role_domain_id=None, + list_status_OK=False, expected=None): status_OK, status_created, status_no_data = self._stati(expected) - a_role = unit.new_role_ref() + a_role = unit.new_role_ref(domain_id=role_domain_id) self.role_api.create_role(a_role['id'], a_role) collection_url = ( @@ -753,8 +754,11 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, expected_status=status_no_data) self.head(member_url, auth=self.auth, expected_status=status_no_data) - self.get(collection_url, auth=self.auth, - expected_status=status_OK) + if list_status_OK: + self.get(collection_url, auth=self.auth) + else: + self.get(collection_url, auth=self.auth, + expected_status=status_OK) self.delete(member_url, auth=self.auth, expected_status=status_no_data) @@ -983,6 +987,52 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self._test_grants('domains', self.domainA['id']) + def test_domain_grants_by_cloud_admin_for_domain_specific_role(self): + # Test domain grants with a cloud admin. This user should be + # able to manage domain roles on any domain. + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + project_id=self.admin_project['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id']) + + def test_domain_grants_by_non_admin_for_domain_specific_role(self): + # A non-admin shouldn't be able to do anything + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainA['id'], + expected=exception.ForbiddenAction.code) + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id'], + expected=exception.ForbiddenAction.code) + + def test_domain_grants_by_domain_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the domain admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainA['id']) + def test_project_grants(self): self.auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -1011,6 +1061,62 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self._test_grants('projects', self.project['id']) + def test_project_grants_by_non_admin_for_domain_specific_role(self): + # A non-admin shouldn't be able to do anything + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id'], + expected=exception.ForbiddenAction.code) + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + expected=exception.ForbiddenAction.code) + + def test_project_grants_by_project_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the project admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id']) + + def test_project_grants_by_domain_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the domain admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id']) + def test_cloud_admin_list_assignments_of_domain(self): self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], diff --git a/releasenotes/notes/DomainSpecificRoles-fc5dd2ef74a1442c.yaml b/releasenotes/notes/DomainSpecificRoles-fc5dd2ef74a1442c.yaml new file mode 100644 index 0000000000..d724c60b4c --- /dev/null +++ b/releasenotes/notes/DomainSpecificRoles-fc5dd2ef74a1442c.yaml @@ -0,0 +1,11 @@ +--- +features: + - > + [`blueprint domain-specific-roles `_] + Roles can now be optionally defined as domain specific. Domain specific + roles are not references in policy files, rather they can be used to allow + a domain to build their own private inference rules with implies roles. A + domain specific role can be assigned to a domain or project within its + domain, and any subset of global roles it implies will appear in a token + scoped to the respective domain or project. The domain specific role + itself, however, will not appear in the token.