diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json index f0b337a95d..8487a34a47 100644 --- a/etc/policy.v3cloudsample.json +++ b/etc/policy.v3cloudsample.json @@ -19,11 +19,6 @@ "identity:ec2_list_credentials": "rule:admin_required or rule:owner", "identity:ec2_create_credential": "rule:admin_required or rule:owner", - "identity:get_domain_role": "rule:cloud_admin or rule:get_domain_roles", - "identity:list_domain_roles": "rule:cloud_admin or rule:list_domain_roles", - "identity:create_domain_role": "rule:cloud_admin or rule:domain_admin_matches_domain_role", - "identity:update_domain_role": "rule:cloud_admin or rule:domain_admin_matches_target_domain_role", - "identity:delete_domain_role": "rule:cloud_admin or rule:domain_admin_matches_target_domain_role", "domain_admin_matches_domain_role": "rule:admin_required and domain_id:%(role.domain_id)s", "get_domain_roles": "rule:domain_admin_matches_target_domain_role or rule:project_admin_matches_target_domain_role", "domain_admin_matches_target_domain_role": "rule:admin_required and domain_id:%(target.role.domain_id)s", diff --git a/keystone/common/policies/role.py b/keystone/common/policies/role.py index 72ee8cec30..571ebdabfa 100644 --- a/keystone/common/policies/role.py +++ b/keystone/common/policies/role.py @@ -35,6 +35,26 @@ deprecated_delete_role = policy.DeprecatedRule( name=base.IDENTITY % 'delete_role', check_str=base.RULE_ADMIN_REQUIRED ) +deprecated_get_domain_role = policy.DeprecatedRule( + name=base.IDENTITY % 'get_domain_role', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_list_domain_roles = policy.DeprecatedRule( + name=base.IDENTITY % 'list_domain_roles', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_update_domain_role = policy.DeprecatedRule( + name=base.IDENTITY % 'update_domain_role', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_create_domain_role = policy.DeprecatedRule( + name=base.IDENTITY % 'create_domain_role', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_delete_domain_role = policy.DeprecatedRule( + name=base.IDENTITY % 'delete_domain_role', + check_str=base.RULE_ADMIN_REQUIRED +) DEPRECATED_REASON = """ As of the Stein release, the role API now understands default roles and @@ -106,7 +126,7 @@ role_policies = [ deprecated_since=versionutils.deprecated.STEIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'get_domain_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, # FIXME(lbragstad): Once OpenStack supports a way to make role changes # without having to modify policy files, scope_types for # domain-specific roles should include `project`. This will expose @@ -117,37 +137,52 @@ role_policies = [ operations=[{'path': '/v3/roles/{role_id}', 'method': 'GET'}, {'path': '/v3/roles/{role_id}', - 'method': 'HEAD'}]), + 'method': 'HEAD'}], + deprecated_rule=deprecated_get_domain_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'list_domain_roles', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, description='List domain roles.', scope_types=['system'], operations=[{'path': '/v3/roles?domain_id={domain_id}', 'method': 'GET'}, {'path': '/v3/roles?domain_id={domain_id}', - 'method': 'HEAD'}]), + 'method': 'HEAD'}], + deprecated_rule=deprecated_list_domain_roles, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'create_domain_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_ADMIN, description='Create domain role.', scope_types=['system'], operations=[{'path': '/v3/roles', - 'method': 'POST'}]), + 'method': 'POST'}], + deprecated_rule=deprecated_create_domain_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'update_domain_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_ADMIN, description='Update domain role.', scope_types=['system'], operations=[{'path': '/v3/roles/{role_id}', - 'method': 'PATCH'}]), + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_domain_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'delete_domain_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_ADMIN, description='Delete domain role.', scope_types=['system'], operations=[{'path': '/v3/roles/{role_id}', - 'method': 'DELETE'}]) + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_domain_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN) ] diff --git a/keystone/tests/unit/protection/v3/test_domain_roles.py b/keystone/tests/unit/protection/v3/test_domain_roles.py new file mode 100644 index 0000000000..86a19cf85b --- /dev/null +++ b/keystone/tests/unit/protection/v3/test_domain_roles.py @@ -0,0 +1,377 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from six.moves import http_client + +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 + +CONF = keystone.conf.CONF +PROVIDERS = provider_api.ProviderAPIs + + +class _SystemUserDomainRoleTests(object): + """Common default functionality for all system users.""" + + def test_user_can_list_domain_roles(self): + PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id)) + + with self.test_client() as c: + r = c.get( + '/v3/roles?domain_id=%s' % CONF.identity.default_domain_id, + headers=self.headers) + self.assertEqual(1, len(r.json['roles'])) + + def test_user_can_get_a_domain_role(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + r = c.get('/v3/roles/%s' % role['id'], headers=self.headers) + self.assertEqual(role['id'], r.json['role']['id']) + + +class _SystemReaderAndMemberDomainRoleTests(object): + """Common default functionality for system readers and system members.""" + + def test_user_cannot_create_domain_roles(self): + create = {'role': unit.new_role_ref( + domain_id=CONF.identity.default_domain_id)} + + with self.test_client() as c: + c.post( + '/v3/roles', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_domain_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + update = {'role': {'description': uuid.uuid4().hex}} + + with self.test_client() as c: + c.patch( + '/v3/roles/%s' % role['id'], json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_domain_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + c.delete( + '/v3/roles/%s' % role['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class _DomainAndProjectUserDomainRoleTests(object): + """Common functionality for all domain and project users.""" + + def test_user_cannot_list_domain_roles(self): + PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id)) + + with self.test_client() as c: + c.get( + '/v3/roles', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_get_a_domain_role(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + c.get( + '/v3/roles/%s' % role['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_create_domain_roles(self): + create = {'role': unit.new_role_ref( + domain_id=CONF.identity.default_domain_id)} + + with self.test_client() as c: + c.post( + '/v3/roles', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_domain_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + update = {'role': {'description': uuid.uuid4().hex}} + + with self.test_client() as c: + c.patch( + '/v3/roles/%s' % role['id'], json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_domain_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, + unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + c.delete( + '/v3/roles/%s' % role['id'], headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class SystemReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserDomainRoleTests, + _SystemReaderAndMemberDomainRoleTests): + + def setUp(self): + super(SystemReaderTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + system_reader = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + system_reader + )['id'] + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.bootstrapper.reader_role_id + ) + + auth = self.build_authentication_request( + user_id=self.user_id, password=system_reader['password'], + system=True + ) + + # 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 SystemMemberTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserDomainRoleTests, + _SystemReaderAndMemberDomainRoleTests): + + def setUp(self): + super(SystemMemberTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + system_member = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + system_member + )['id'] + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.bootstrapper.member_role_id + ) + + auth = self.build_authentication_request( + user_id=self.user_id, password=system_member['password'], + system=True + ) + + # 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 SystemAdminTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserDomainRoleTests): + + def setUp(self): + super(SystemAdminTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + # Reuse the system administrator account created during + # ``keystone-manage bootstrap`` + self.user_id = self.bootstrapper.admin_user_id + auth = self.build_authentication_request( + user_id=self.user_id, + password=self.bootstrapper.admin_password, + system=True + ) + + # 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 test_user_can_create_roles(self): + create = {'role': unit.new_role_ref(domain_id=CONF.identity.default_domain_id)} + + with self.test_client() as c: + c.post('/v3/roles', json=create, headers=self.headers) + + def test_user_can_update_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + update = {'role': {'description': uuid.uuid4().hex}} + + with self.test_client() as c: + c.patch( + '/v3/roles/%s' % role['id'], json=update, headers=self.headers, + ) + + def test_user_can_delete_roles(self): + role = PROVIDERS.role_api.create_role( + uuid.uuid4().hex, unit.new_role_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + c.delete('/v3/roles/%s' % role['id'], headers=self.headers) + + +class DomainUserTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _DomainAndProjectUserDomainRoleTests): + + def setUp(self): + super(DomainUserTests, 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'] + domain_admin = unit.new_user_ref(domain_id=self.domain_id) + self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.admin_role_id, user_id=self.user_id, + domain_id=self.domain_id + ) + + auth = self.build_authentication_request( + user_id=self.user_id, + password=domain_admin['password'], + domain_id=self.domain_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 ProjectUserTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _DomainAndProjectUserDomainRoleTests): + + def setUp(self): + super(ProjectUserTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + 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} + + +class ProjectUserTestsWithoutEnforceScope( + base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _DomainAndProjectUserDomainRoleTests): + + def setUp(self): + super(ProjectUserTestsWithoutEnforceScope, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + + # Explicityly set enforce_scope to False to make sure we maintain + # backwards compatibility with project users. + self.config_fixture.config(group='oslo_policy', enforce_scope=False) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + user = unit.new_user_ref(domain_id=domain['id']) + self.user_id = PROVIDERS.identity_api.create_user(user)['id'] + + self.project_id = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, unit.new_project_ref(domain_id=domain['id']) + )['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=user['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} diff --git a/keystone/tests/unit/test_policy.py b/keystone/tests/unit/test_policy.py index 3e4cf8481a..37c453e220 100644 --- a/keystone/tests/unit/test_policy.py +++ b/keystone/tests/unit/test_policy.py @@ -346,6 +346,11 @@ class PolicyJsonTestCase(unit.TestCase): 'identity:delete_implied_role', 'identity:list_role_inference_rules', 'identity:check_implied_role', + 'identity:get_domain_role', + 'identity:list_domain_roles', + 'identity:create_domain_role', + 'identity:update_domain_role', + 'identity:delete_domain_role', ] policy_keys = self._get_default_policy_rules() for p in removed_policies: diff --git a/keystone/tests/unit/test_v3_protection.py b/keystone/tests/unit/test_v3_protection.py index e24f639cbd..ac2e42ee39 100644 --- a/keystone/tests/unit/test_v3_protection.py +++ b/keystone/tests/unit/test_v3_protection.py @@ -1633,78 +1633,6 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self._role_management_cases() - def test_domain_role_management_no_admin_no_rights(self): - # A non-admin domain user shouldn't be able to manipulate domain roles - 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._domain_role_management_cases( - self.domainA['id'], expected=exception.ForbiddenAction.code) - - # ...and nor should non-admin project user - 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._domain_role_management_cases( - self.domainA['id'], expected=exception.ForbiddenAction.code) - - def test_domain_role_management_with_cloud_admin(self): - # A cloud admin user should have rights to manipulate domain roles - 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._domain_role_management_cases(self.domainA['id']) - - def test_domain_role_management_with_domain_admin(self): - # A domain admin user should only be able to manipulate the domain - # specific roles in their own domain - self.auth = self.build_authentication_request( - user_id=self.domainB_admin_user['id'], - password=self.domainB_admin_user['password'], - domain_id=self.domainB['id']) - - # Try to access the domain specific roles in another domain - self._domain_role_management_cases( - self.domainA['id'], expected=exception.ForbiddenAction.code) - - # ...but they should be able to work with those in their own 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._domain_role_management_cases(self.domainA['id']) - - def test_domain_role_management_with_project_admin(self): - # A project admin user should have not access to domain specific roles - # in another domain. They should be able to get and list domain - # specific roles from their own domain, but not be able to create, - # update or delete them, - self.auth = self.build_authentication_request( - user_id=self.project_adminB_user['id'], - password=self.project_adminB_user['password'], - project_id=self.projectB['id']) - - # Try access the domain specific roless in another domain - self._domain_role_management_cases( - self.domainA['id'], expected=exception.ForbiddenAction.code) - - # ...but they should be ablet to work with those in their own 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._domain_role_management_cases( - self.domainA['id'], read_status_OK=True, - expected=exception.ForbiddenAction.code) - class IdentityTestImpliedDomainSpecificRoles(IdentityTestv3CloudPolicySample): """Test Domain specific Implied Roles via the REST API.""" diff --git a/releasenotes/notes/bug-1805400-c192be936d277ade.yaml b/releasenotes/notes/bug-1805400-c192be936d277ade.yaml new file mode 100644 index 0000000000..2a8a9c3e2a --- /dev/null +++ b/releasenotes/notes/bug-1805400-c192be936d277ade.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + [`bug 1805400 `_] + The domain roles API now supports system scope using the ``admin``, + ``member``, and ``reader`` default roles. +upgrade: + - | + [`bug 1805400 `_] + The domain role 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 role + policies. +deprecations: + - | + [`bug 1805400 `_] + The domain role policies have been deprecated. The + ``identity:get_domain_role`` and ``identity:list_domain_roles`` policies + now use ``role:reader and system_scope:all`` instead of + ``rule:admin_required``. The ``identity:create_domain_role``, + ``identity:update_domain_role``, and ``identity:delete_role`` policies now + use ``role:admin and system_scope:all`` instead of ``rule:admin_required``. + These new defaults automatically account for system-scope and support a + read-only role, making it easier for system administrators to delegate + subsets of responsibility without compromising security. Please consider + these new defaults if your deployment overrides the domain role policies. +security: + - | + [`bug 1805400 `_] + The domain role API now uses system-scope and default roles to provide + better accessibility to users in a secure way. +