From 00c2ecdf333c4026990fd04a44ce3692345ff4cc Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 6 Sep 2019 20:25:07 -0700 Subject: [PATCH] Implement system reader for implied roles This change updates the implied roles policies to understand the system reader role, and includes tests for system reader and system member. A follow-on change will add support for system admin. For the time being, we're deferring adding support for domain users to create implied roles but may add it in the future. Change-Id: I71f80eed5cd689505c7fd07f133e74f0e8edf66a Partial-bug: #1805371 --- keystone/common/policies/implied_role.py | 55 +++++- .../unit/protection/v3/test_implied_roles.py | 171 ++++++++++++++++++ 2 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 keystone/tests/unit/protection/v3/test_implied_roles.py diff --git a/keystone/common/policies/implied_role.py b/keystone/common/policies/implied_role.py index 2a35b28e81..8e4d911949 100644 --- a/keystone/common/policies/implied_role.py +++ b/keystone/common/policies/implied_role.py @@ -10,14 +10,41 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import versionutils from oslo_policy import policy from keystone.common.policies import base +deprecated_get_implied_role = policy.DeprecatedRule( + name=base.IDENTITY % 'get_implied_role', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_list_implied_roles = policy.DeprecatedRule( + name=base.IDENTITY % 'list_implied_roles', + check_str=base.RULE_ADMIN_REQUIRED, +) +deprecated_list_role_inference_rules = policy.DeprecatedRule( + name=base.IDENTITY % 'list_role_inference_rules', + check_str=base.RULE_ADMIN_REQUIRED, +) +deprecated_check_implied_role = policy.DeprecatedRule( + name=base.IDENTITY % 'check_implied_role', + check_str=base.RULE_ADMIN_REQUIRED, +) + +DEPRECATED_REASON = """ +As of the Train release, the implied role API understands how to +handle system-scoped tokens in addition to project tokens, making the API +more accessible to users without compromising security or manageability for +administrators. The new default policies for this API account for these changes +automatically. +""" + + implied_role_policies = [ policy.DocumentedRuleDefault( name=base.IDENTITY % 'get_implied_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, # FIXME(lbragstad) The management of implied roles currently makes # sense as a system-only resource. Once keystone has the ability to # support RBAC solely over the API without having to customize policy @@ -29,10 +56,13 @@ implied_role_policies = [ 'the user also assumes the implied role.', operations=[ {'path': '/v3/roles/{prior_role_id}/implies/{implied_role_id}', - 'method': 'GET'}]), + 'method': 'GET'}], + deprecated_rule=deprecated_get_implied_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'list_implied_roles', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, scope_types=['system'], description='List associations between two roles. When a relationship ' 'exists between a prior role and an implied role and the ' @@ -42,7 +72,10 @@ implied_role_policies = [ 'prior role.', operations=[ {'path': '/v3/roles/{prior_role_id}/implies', 'method': 'GET'}, - {'path': '/v3/roles/{prior_role_id}/implies', 'method': 'HEAD'}]), + {'path': '/v3/roles/{prior_role_id}/implies', 'method': 'HEAD'}], + deprecated_rule=deprecated_list_implied_roles, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'create_implied_role', check_str=base.RULE_ADMIN_REQUIRED, @@ -68,7 +101,7 @@ implied_role_policies = [ 'method': 'DELETE'}]), policy.DocumentedRuleDefault( name=base.IDENTITY % 'list_role_inference_rules', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, scope_types=['system'], description='List all associations between two roles in the system. ' 'When a relationship exists between a prior role and an ' @@ -76,10 +109,13 @@ implied_role_policies = [ 'the user also assumes the implied role.', operations=[ {'path': '/v3/role_inferences', 'method': 'GET'}, - {'path': '/v3/role_inferences', 'method': 'HEAD'}]), + {'path': '/v3/role_inferences', 'method': 'HEAD'}], + deprecated_rule=deprecated_list_role_inference_rules, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'check_implied_role', - check_str=base.RULE_ADMIN_REQUIRED, + check_str=base.SYSTEM_READER, scope_types=['system'], description='Check an association between two roles. When a ' 'relationship exists between a prior role and an implied ' @@ -87,7 +123,10 @@ implied_role_policies = [ 'also assumes the implied role.', operations=[ {'path': '/v3/roles/{prior_role_id}/implies/{implied_role_id}', - 'method': 'HEAD'}]) + 'method': 'HEAD'}], + deprecated_rule=deprecated_check_implied_role, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), ] diff --git a/keystone/tests/unit/protection/v3/test_implied_roles.py b/keystone/tests/unit/protection/v3/test_implied_roles.py new file mode 100644 index 0000000000..a974bd460d --- /dev/null +++ b/keystone/tests/unit/protection/v3/test_implied_roles.py @@ -0,0 +1,171 @@ +# 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. + +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 _ImpliedRolesSetupMixin(object): + def _create_test_roles(self): + ref = unit.new_role_ref() + role = PROVIDERS.role_api.create_role(ref['id'], ref) + self.prior_role_id = role['id'] + ref = unit.new_role_ref() + role = PROVIDERS.role_api.create_role(ref['id'], ref) + self.implied_role_id = role['id'] + + +class _SystemUserImpliedRoleTests(object): + """Common default functionality for all system users.""" + + def test_user_can_list_implied_roles(self): + PROVIDERS.role_api.create_implied_role(self.prior_role_id, + self.implied_role_id) + + with self.test_client() as c: + r = c.get('/v3/roles/%s/implies' % self.prior_role_id, + headers=self.headers) + self.assertEqual(1, len(r.json['role_inference']['implies'])) + + def test_user_can_get_an_implied_role(self): + PROVIDERS.role_api.create_implied_role(self.prior_role_id, + self.implied_role_id) + + with self.test_client() as c: + c.get( + '/v3/roles/%s/implies/%s' % ( + self.prior_role_id, self.implied_role_id), + headers=self.headers) + c.head( + '/v3/roles/%s/implies/%s' % ( + self.prior_role_id, self.implied_role_id), + headers=self.headers, + expected_status_code=http_client.NO_CONTENT) + + def test_user_can_list_role_inference_rules(self): + PROVIDERS.role_api.create_implied_role(self.prior_role_id, + self.implied_role_id) + + with self.test_client() as c: + r = c.get('/v3/role_inferences', + headers=self.headers) + # There should be three role inferences: two from the defaults and + # one from the test setup + self.assertEqual(3, len(r.json['role_inferences'])) + + +class _SystemReaderAndMemberImpliedRoleTests(object): + """Common default functionality for system readers and system members.""" + + def test_user_cannot_create_implied_roles(self): + with self.test_client() as c: + c.put( + '/v3/roles/%s/implies/%s' % ( + self.prior_role_id, self.implied_role_id), + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_implied_roles(self): + PROVIDERS.role_api.create_implied_role(self.prior_role_id, + self.implied_role_id) + + with self.test_client() as c: + c.delete( + '/v3/roles/%s/implies/%s' % ( + self.prior_role_id, self.implied_role_id), + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class SystemReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _ImpliedRolesSetupMixin, + _SystemUserImpliedRoleTests, + _SystemReaderAndMemberImpliedRoleTests): + + 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) + + self._create_test_roles() + + 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, + _ImpliedRolesSetupMixin, + _SystemUserImpliedRoleTests, + _SystemReaderAndMemberImpliedRoleTests): + + 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) + + self._create_test_roles() + + 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}