From f9e07a940dc6d197709f5a66c8e4ff203d388da1 Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Thu, 6 Dec 2018 21:01:14 +0000 Subject: [PATCH] Add explicit testing for project users and the user API This commit wraps up the user API policy refactor by adding explicit testing for how project users are expected to behave with the user API. A subsequent patch set will remove the now obsolete user policies in policy.v3cloudsample.json. Change-Id: Ic7b0839ac70439aa0311a98c6b7b5688a7e2dcf7 Closes-Bug: 1748027 Related-Bug: 968696 --- .../tests/unit/protection/v3/test_users.py | 298 ++++++++++++++++++ .../notes/bug-1748027-decc2e11154b97cf.yaml | 39 +++ 2 files changed, 337 insertions(+) create mode 100644 releasenotes/notes/bug-1748027-decc2e11154b97cf.yaml diff --git a/keystone/tests/unit/protection/v3/test_users.py b/keystone/tests/unit/protection/v3/test_users.py index c8da2fa210..ae5317efce 100644 --- a/keystone/tests/unit/protection/v3/test_users.py +++ b/keystone/tests/unit/protection/v3/test_users.py @@ -300,6 +300,165 @@ class _DomainMemberAndReaderUserTests(object): ) +class _ProjectUserTests(object): + """Common tests cases for all project users.""" + + def test_user_cannot_get_users_within_their_domain(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=self.domain_id) + ) + + with self.test_client() as c: + c.get( + '/v3/users/%s' % user['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_get_users_in_other_domains(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=domain['id']) + ) + + with self.test_client() as c: + c.get( + '/v3/users/%s' % user['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_get_non_existent_user_forbidden(self): + with self.test_client() as c: + c.get( + '/v3/users/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_users_within_domain(self): + with self.test_client() as c: + c.get( + '/v3/users?domain_id=%s' % self.domain_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_users_in_other_domains(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=domain['id']) + ) + + with self.test_client() as c: + c.get( + '/v3/users?domain_id=%s' % domain['id'], + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_create_users_within_domain(self): + create = { + 'user': { + 'domain_id': self.domain_id, + 'name': uuid.uuid4().hex + } + } + + with self.test_client() as c: + c.post( + '/v3/users', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_create_users_in_other_domains(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + + create = { + 'user': { + 'domain_id': domain['id'], + 'name': uuid.uuid4().hex + } + } + + with self.test_client() as c: + c.post( + '/v3/users', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_users_within_domain(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=self.domain_id) + ) + + update = {'user': {'email': uuid.uuid4().hex}} + with self.test_client() as c: + c.patch( + '/v3/users/%s' % user['id'], json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_users_in_other_domain(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=domain['id']) + ) + + update = {'user': {'email': uuid.uuid4().hex}} + with self.test_client() as c: + c.patch( + '/v3/users/%s' % user['id'], json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_non_existent_user_forbidden(self): + update = {'user': {'email': uuid.uuid4().hex}} + with self.test_client() as c: + c.patch( + '/v3/users/%s' % uuid.uuid4().hex, json=update, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_users_within_domain(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=self.domain_id) + ) + + with self.test_client() as c: + c.delete( + '/v3/users/%s' % user['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_users_in_other_domains(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=domain['id']) + ) + + with self.test_client() as c: + c.delete( + '/v3/users/%s' % user['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_non_existent_user_forbidden(self): + with self.test_client() as c: + c.delete( + '/v3/users/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + class SystemReaderTests(base_classes.TestCaseWithBootstrap, common_auth.AuthTestMixin, _CommonUserTests, @@ -670,3 +829,142 @@ class DomainAdminTests(base_classes.TestCaseWithBootstrap, '/v3/users/%s' % uuid.uuid4().hex, headers=self.headers, expected_status_code=http_client.FORBIDDEN ) + + +class ProjectReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _CommonUserTests, + _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 + ) + + 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, + _CommonUserTests, + _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 + ) + + 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, + _CommonUserTests, + _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 + 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.users 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:get_user': up.SYSTEM_READER_OR_DOMAIN_READER_OR_USER, + 'identity:list_users': up.SYSTEM_READER_OR_DOMAIN_READER, + 'identity:create_user': up.SYSTEM_ADMIN_OR_DOMAIN_ADMIN, + 'identity:update_user': up.SYSTEM_ADMIN_OR_DOMAIN_ADMIN, + 'identity:delete_user': up.SYSTEM_ADMIN_OR_DOMAIN_ADMIN + } + f.write(jsonutils.dumps(overridden_policies)) diff --git a/releasenotes/notes/bug-1748027-decc2e11154b97cf.yaml b/releasenotes/notes/bug-1748027-decc2e11154b97cf.yaml new file mode 100644 index 0000000000..eff93c5a03 --- /dev/null +++ b/releasenotes/notes/bug-1748027-decc2e11154b97cf.yaml @@ -0,0 +1,39 @@ +--- +features: + - | + [`bug 1748027 `_] + The user API now supports the ``admin``, ``member``, and + ``reader`` default roles across system-scope, domain-scope, and + project-scope. +upgrade: + - | + [`bug 1748027 `_] + The user API uses new default policies that make it more + accessible to end users and administrators in a secure way. Please + consider these new defaults if your deployment overrides + user policies. +deprecations: + - | + [`bug 1748027 `_] + The user policies have been deprecated. The ``identity:get_user`` + policy now uses ``(role:reader and system_scope:all) or + (role:reader and token.domain.id:%(target.user.domain_id)s) or + user_id:%(target.user.id)s`` instead of ``rule:admin_or_owner``. + The ``identity:list_users`` policy now uses ``(role:reader and + system_scope:all) or (role:reader and + domain_id:%(target.domain_id)s)`` instead of + ``rule:admin_required``. The ``identity:create_user``, + ``identity:update_user``, and ``identity:delete_user`` policies + now use ``(role:admin and system_scope:all) or (role:admin and + token.domain.id:%(target.user.domain_id)s)`` instead of + ``rule:admin_required``. These new defaults automatically include + support for a read-only role and allow for more granular access to + user APIs, making it easier for system and domain administrators + to delegate authorization, safely. Please consider these new + defaults if your deployment overrides user policies. +security: + - | + [`bug 1748027 `_] + The user API now uses system-scope, domain-scope, project-scope and default + roles to provide better accessibility to users in a secure way. +