From 2c8f81af62cd03601fca259647991d5dd7f8d560 Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Thu, 27 Sep 2018 21:51:12 +0000 Subject: [PATCH] Allow project users to retrieve domains This commit adds thorough testing to make sure users who have a role on a project can use project-scoped tokens to call GET /v3/domain/{domain_id} for the domain own their project. These users are not allowed to access domains that they don't have any authorization via project role assignments. This ensures the domains API is tested with these cases and makes the domains API more self-serviceable for users that are not administrators. Change-Id: Ifc100a7a235140fbd07cbafe80983d3c2f17a7dc Closes-Bug: 1794864 Related-Bug: 968696 --- keystone/common/policies/domain.py | 12 +- .../tests/unit/protection/v3/test_domains.py | 156 ++++++++++++++++++ .../notes/bug-1794864-3116bf165a146be6.yaml | 41 +++++ 3 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/bug-1794864-3116bf165a146be6.yaml diff --git a/keystone/common/policies/domain.py b/keystone/common/policies/domain.py index 2a8238ed12..342bcdf615 100644 --- a/keystone/common/policies/domain.py +++ b/keystone/common/policies/domain.py @@ -43,17 +43,19 @@ deprecated_delete_domain = policy.DeprecatedRule( name=base.IDENTITY % 'delete_domain', check_str=base.RULE_ADMIN_REQUIRED ) +SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER = ( + '(role:reader and system_scope:all) or ' + 'token.domain.id:%(target.domain.id)s or ' + 'token.project.domain.id:%(target.domain.id)s' +) + domain_policies = [ policy.DocumentedRuleDefault( name=base.IDENTITY % 'get_domain', # NOTE(lbragstad): This policy allows system, domain, and # project-scoped tokens. - check_str=( - '(role:reader and system_scope:all) or ' - 'token.domain.id:%(target.domain.id)s or ' - 'token.project.domain.id:%(target.domain.id)s' - ), + check_str=SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER, scope_types=['system', 'domain', 'project'], description='Show domain details.', operations=[{'path': '/v3/domains/{domain_id}', diff --git a/keystone/tests/unit/protection/v3/test_domains.py b/keystone/tests/unit/protection/v3/test_domains.py index 43136a6c74..5997ac986f 100644 --- a/keystone/tests/unit/protection/v3/test_domains.py +++ b/keystone/tests/unit/protection/v3/test_domains.py @@ -12,14 +12,17 @@ import uuid +from oslo_serialization import jsonutils from six.moves import http_client +from keystone.common.policies import domain as dp 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 @@ -384,3 +387,156 @@ class DomainUserTests(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, + _DomainAndProjectUserDomainTests): + + 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_reader = unit.new_user_ref(domain_id=self.domain_id) + project_reader_id = PROVIDERS.identity_api.create_user( + project_reader + )['id'] + project = unit.new_project_ref(domain_id=self.domain_id) + project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=project_reader_id, + project_id=project_id + ) + + auth = self.build_authentication_request( + user_id=project_reader_id, + password=project_reader['password'], + project_id=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, + _DomainAndProjectUserDomainTests): + + 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_member = unit.new_user_ref(domain_id=self.domain_id) + project_member_id = PROVIDERS.identity_api.create_user( + project_member + )['id'] + project = unit.new_project_ref(domain_id=self.domain_id) + project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=project_member_id, + project_id=project_id + ) + + auth = self.build_authentication_request( + user_id=project_member_id, + password=project_member['password'], + project_id=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, + _DomainAndProjectUserDomainTests): + + 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'] + + project_admin = unit.new_user_ref(domain_id=self.domain_id) + project_admin_id = PROVIDERS.identity_api.create_user( + project_admin + )['id'] + project = unit.new_project_ref(domain_id=self.domain_id) + project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.admin_role_id, user_id=project_admin_id, + project_id=project_id + ) + + auth = self.build_authentication_request( + user_id=project_admin_id, + password=project_admin['password'], + project_id=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.domains 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_domain': ( + dp.SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER + ) + } + f.write(jsonutils.dumps(overridden_policies)) diff --git a/releasenotes/notes/bug-1794864-3116bf165a146be6.yaml b/releasenotes/notes/bug-1794864-3116bf165a146be6.yaml new file mode 100644 index 0000000000..267c8d5c12 --- /dev/null +++ b/releasenotes/notes/bug-1794864-3116bf165a146be6.yaml @@ -0,0 +1,41 @@ +--- +upgrade: + - | + [`bug 1794864 `_] + [`bug 1794376 `_] + The default policies that protect the domains API have been deprecated in + favor of ones that are more secure and self-serviceable. If you're + maintaining custom policies, please make sure you resolve your domain + policies to work with the new default by adding the proper role + assignments, or continue maintaining custom overrides. The new defaults + allow for better protection of the domains API when giving the `admin` role + to users on domains and projects. +deprecations: + - | + [`bug 1794864 `_] + [`bug 1794376 `_] + The default policies that protect the domains API have been deprecated in + favor of ones that are more secure and self-serviceable. If you're + maintaining custom policies, please make sure you resolve your domain + policies to work with the new default by adding the proper role + assignments, or continue maintaining custom overrides. The new defaults + allow for better protection of the domains API when giving the `admin` role + to users on domains and projects. +security: + - | + [`bug 1794864 `_] + [`bug 1794376 `_] + The default policies that protect the domains API have been deprecated in + favor of ones that are more secure and self-serviceable. +fixes: + - | + [`bug 1794864 `_] + [`bug 1794376 `_] + The default policies that protect the domains API have been deprecated in + favor of ones that are more secure and self-serviceable. Users with roles + on domains and projects are now able to call the + ``GET /v3/domains/{domain_id}`` API if they use a token scoped to that + domain or a token scoped to a project within that domain. System users are + allowed to access the domain APIs in the same way legacy `admin` users were + able to. This allows for better protection of the domain API when giving + the `admin` role to users on domains and projects.