diff --git a/keystone/common/policies/role_assignment.py b/keystone/common/policies/role_assignment.py index 2dda64d00a..e0dc9d7777 100644 --- a/keystone/common/policies/role_assignment.py +++ b/keystone/common/policies/role_assignment.py @@ -37,14 +37,6 @@ role_assignment_policies = [ policy.DocumentedRuleDefault( name=base.IDENTITY % 'list_role_assignments', check_str=SYSTEM_READER_OR_DOMAIN_READER, - # FIXME(lbragstad): This API will behave differently depending on the - # token scope used to call the API. A system administrator should be - # able to list all role assignment across the entire deployment. A - # project or domain administrator should be able to list role - # assignments within the domain or project they administer. Once we - # make keystone smart enough to handle those cases in code, we can add - # 'project' to the scope_types below. For now, this should be a system - # administrator only operation to maintain backwards compatibility. scope_types=['system', 'domain'], description='List role assignments.', operations=[{'path': '/v3/role_assignments', diff --git a/keystone/tests/unit/protection/v3/test_assignment.py b/keystone/tests/unit/protection/v3/test_assignment.py index 5616546411..a5bc13d44c 100644 --- a/keystone/tests/unit/protection/v3/test_assignment.py +++ b/keystone/tests/unit/protection/v3/test_assignment.py @@ -10,14 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. +from six.moves import http_client import uuid +from oslo_serialization import jsonutils + +from keystone.common.policies import role_assignment as rp from keystone.common import provider_api import keystone.conf from keystone.tests.common import auth as common_auth from keystone.tests import unit from keystone.tests.unit import base_classes from keystone.tests.unit import ksfixtures +from keystone.tests.unit.ksfixtures import temporaryfile CONF = keystone.conf.CONF PROVIDERS = provider_api.ProviderAPIs @@ -918,6 +923,99 @@ class _DomainUserTests(object): self.assertEqual(0, len(r.json['role_assignments'])) +class _ProjectUserTests(object): + + def test_user_cannot_list_all_assignments_in_their_project(self): + with self.test_client() as c: + c.get( + '/v3/role_assignments', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_user_of_project(self): + assignments = self._setup_test_role_assignments() + user_id = assignments['user_id'] + + with self.test_client() as c: + c.get( + '/v3/role_assignments?user.id=%s' % user_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_group_of_project(self): + assignments = self._setup_test_role_assignments() + group_id = assignments['group_id'] + + with self.test_client() as c: + c.get( + '/v3/role_assignments?group.id=%s' % group_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_system(self): + with self.test_client() as c: + c.get( + '/v3/role_assignments?scope.system=all', + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_domain(self): + with self.test_client() as c: + c.get( + '/v3/role_assignments?scope.domain.id=%s' + % self.domain_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_other_project(self): + project1 = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, + unit.new_project_ref(domain_id=self.domain_id) + ) + + with self.test_client() as c: + c.get( + '/v3/role_assignments?scope.project.id=%s' + % project1, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_other_project_user(self): + assignments = self._setup_test_role_assignments() + + # This user doesn't have any role assignments on self.project_id, so the + # project user of self.project_id should only see an empty list of role + # assignments. + user_id = assignments['user_id'] + + with self.test_client() as c: + c.get( + '/v3/role_assignments?user.id=%s' % user_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_filter_role_assignments_by_other_project_group(self): + assignments = self._setup_test_role_assignments() + + # This group doesn't have any role assignments on self.project_id, so + # the project user of self.project_id should only see an empty list of + # role assignments. + group_id = assignments['group_id'] + + with self.test_client() as c: + c.get( + '/v3/role_assignments?group.id=%s' % group_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + class SystemReaderTests(base_classes.TestCaseWithBootstrap, common_auth.AuthTestMixin, _AssignmentTestUtilities, @@ -1153,3 +1251,172 @@ class DomainAdminTests(base_classes.TestCaseWithBootstrap, r = c.post('/v3/auth/tokens', json=auth) self.token_id = r.headers['X-Subject-Token'] self.headers = {'X-Auth-Token': self.token_id} + + +class ProjectReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _AssignmentTestUtilities, + _ProjectUserTests): + + def setUp(self): + super(ProjectReaderTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + + project = unit.new_project_ref(domain_id=self.domain_id) + project = PROVIDERS.resource_api.create_project(project['id'], project) + self.project_id = project['id'] + + project_reader = unit.new_user_ref(domain_id=self.domain_id) + self.user_id = PROVIDERS.identity_api.create_user(project_reader)['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=self.user_id, + project_id=self.project_id + ) + + self.expected = [ + # assignment of the user running the test case + { + 'user_id': self.user_id, + 'project_id': self.project_id, + 'role_id': self.bootstrapper.reader_role_id + }] + + auth = self.build_authentication_request( + user_id=self.user_id, password=project_reader['password'], + project_id=self.project_id, + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + +class ProjectMemberTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _AssignmentTestUtilities, + _ProjectUserTests): + + def setUp(self): + super(ProjectMemberTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + + project = unit.new_project_ref(domain_id=self.domain_id) + project = PROVIDERS.resource_api.create_project(project['id'], project) + self.project_id = project['id'] + + project_member = unit.new_user_ref(domain_id=self.domain_id) + self.user_id = PROVIDERS.identity_api.create_user(project_member)['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=self.user_id, + project_id=self.project_id + ) + + self.expected = [ + # assignment of the user running the test case + { + 'user_id': self.user_id, + 'project_id': self.project_id, + 'role_id': self.bootstrapper.member_role_id + }] + + auth = self.build_authentication_request( + user_id=self.user_id, password=project_member['password'], + project_id=self.project_id, + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + +class ProjectAdminTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _AssignmentTestUtilities, + _ProjectUserTests): + + def setUp(self): + super(ProjectAdminTests, self).setUp() + self.loadapp() + self.policy_file = self.useFixture(temporaryfile.SecureTempFile()) + self.policy_file_name = self.policy_file.file_name + self.useFixture( + ksfixtures.Policy( + self.config_fixture, policy_file=self.policy_file_name + ) + ) + self._override_policy() + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + + self.user_id = self.bootstrapper.admin_user_id + + project = unit.new_project_ref(domain_id=self.domain_id) + project = PROVIDERS.resource_api.create_project(project['id'], project) + self.project_id = project['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.admin_role_id, user_id=self.user_id, + project_id=self.project_id + ) + + self.expected = [ + # assignment of the user running the test case + { + 'user_id': self.user_id, + 'project_id': self.project_id, + 'role_id': self.bootstrapper.admin_role_id + }] + + auth = self.build_authentication_request( + user_id=self.user_id, + password=self.bootstrapper.admin_password, + project_id=self.bootstrapper.project_id + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + def _override_policy(self): + # TODO(lbragstad): Remove this once the deprecated policies in + # keystone.common.policies.role_assignment have been removed. This is + # only here to make sure we test the new policies instead of the + # deprecated ones. Oslo.policy will OR deprecated policies with new + # policies to maintain compatibility and give operators a chance to + # update permissions or update policies without breaking users. This + # will cause these specific tests to fail since we're trying to correct + # this broken behavior with better scope checking. + with open(self.policy_file_name, 'w') as f: + overridden_policies = { + 'identity:list_role_assignments': ( + rp.SYSTEM_READER_OR_DOMAIN_READER + ) + } + f.write(jsonutils.dumps(overridden_policies)) diff --git a/releasenotes/notes/bug-1750673-b53f74944d767ae9.yaml b/releasenotes/notes/bug-1750673-b53f74944d767ae9.yaml new file mode 100644 index 0000000000..69dfb94f66 --- /dev/null +++ b/releasenotes/notes/bug-1750673-b53f74944d767ae9.yaml @@ -0,0 +1,30 @@ +--- +features: + - | + [`bug 1750673 `_] + The role assignment API now supports the ``admin``, ``member``, and + ``reader`` default roles across system-scope, domain-scope, and + project-scope. +upgrade: + - | + [`bug 1750673 `_] + The role assignment API uses new default policies that make it more + accessible to end users and administrators in a secure way. Please + consider these new policies if your deployment overrides role + assignment policies. +deprecations: + - | + [`bug 1750673 `_] + The role assignment ``identity:list_role_assignments`` policy now + uses ``(role:reader and system_scope:all) or (role:reader and + domain_id:%(target.domain.id)s)`` instead of ``rule:admin_required``. + This new default automatically includes support for a read-only role + and allows for more granular access to the role assignment API. Please + consider this new default if your deployment overrides the role + assignment policies. +security: + - | + [`bug 1750673 `_] + The role assignment API now uses system-scope, domain-scope, + project-scope, and default roles to provide better accessbility to + users in a secure way.