From 5086709ae2d673716653cd8812247ea5a1cb5e69 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 9 Aug 2019 21:37:23 -0700 Subject: [PATCH] Add protection tests for trusts API Currently, the majority of access control enforcement for the trusts API is not done in policy, but hardcoded in the controller logic. The default policy check strings for these routes are empty. Before we can enable system scope and default roles through the trusts policy, we need to replicate the existing access control in policy. To do that, we should test how it currently works. This patch adds those tests. The trusts API is mostly only useable by the trustor or trustee. Mostly, admins can't perform trust actions on behalf of the trustor or trustee. The exception is for the delete action, but only when the is_admin context is set. This change also fixes a minor regression where the is_admin admin could not perform this action due to the auth_context not being populated. Change-Id: I6a5eca8240aa905e02fbf9bec335996c3a4f1c79 Partial-bug: #1818846 Partial-bug: #1818850 --- keystone/api/trusts.py | 2 + .../tests/unit/protection/v3/test_trusts.py | 503 ++++++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 keystone/tests/unit/protection/v3/test_trusts.py diff --git a/keystone/api/trusts.py b/keystone/api/trusts.py index 1573e209d9..9ac1718004 100644 --- a/keystone/api/trusts.py +++ b/keystone/api/trusts.py @@ -85,6 +85,8 @@ class TrustResource(ks_flask.ResourceBase): json_home_parameter_rel_func = _build_parameter_relation def _check_unrestricted(self): + if self.oslo_context.is_admin: + return token = self.auth_context['token'] if 'application_credential' in token.methods: if not token.application_credential['unrestricted']: diff --git a/keystone/tests/unit/protection/v3/test_trusts.py b/keystone/tests/unit/protection/v3/test_trusts.py new file mode 100644 index 0000000000..15b0902734 --- /dev/null +++ b/keystone/tests/unit/protection/v3/test_trusts.py @@ -0,0 +1,503 @@ +# 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 TrustTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin): + """Common functionality for all trust tests. + + Sets up trustor and trustee users and trust. + """ + + def setUp(self): + super(TrustTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + + trustor_user = unit.new_user_ref(domain_id=self.domain_id) + self.trustor_user_id = PROVIDERS.identity_api.create_user( + trustor_user)['id'] + trustee_user = unit.new_user_ref(domain_id=self.domain_id) + self.trustee_user_id = PROVIDERS.identity_api.create_user( + trustee_user)['id'] + project = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id) + ) + self.project_id = project['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=self.trustor_user_id, + project_id=self.project_id + ) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=self.trustee_user_id, + project_id=project['id'] + ) + self.trust_id = uuid.uuid4().hex + self.trust_data = { + 'trust': {'trustor_user_id': self.trustor_user_id, + 'trustee_user_id': self.trustee_user_id, + 'project_id': self.project_id, + 'impersonation': False}, + 'roles': [{"id": self.bootstrapper.member_role_id}] + } + auth = self.build_authentication_request( + user_id=self.trustor_user_id, + password=trustor_user['password'], + project_id=project['id'] + ) + # Grab a token using the trustor 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.trustor_headers = {'X-Auth-Token': self.token_id} + + auth = self.build_authentication_request( + user_id=self.trustee_user_id, + password=trustee_user['password'], + project_id=project['id'] + ) + # Grab a token using the trustee 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.trustee_headers = {'X-Auth-Token': self.token_id} + + +class _AdminTestsMixin(object): + """Tests for all admin users. + + This exercises both the is_admin user and users granted the admin role on + the system scope. + """ + + def test_admin_cannot_create_trust_for_other_user(self): + json = {'trust': self.trust_data['trust']} + json['trust']['roles'] = self.trust_data['roles'] + + with self.test_client() as c: + c.post( + '/v3/OS-TRUST/trusts', + json=json, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_admin_list_all_trusts(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + '/v3/OS-TRUST/trusts', + headers=self.headers + ) + self.assertEqual(1, len(r.json['trusts'])) + + def test_admin_cannot_get_trust_for_other_user(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s' % self.trust_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_admin_can_get_non_existent_trust_not_found(self): + trust_id = uuid.uuid4().hex + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s' % trust_id, + headers=self.headers, + expected_status_code=http_client.NOT_FOUND + ) + + def test_admin_cannot_list_trust_roles_for_other_user(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s/roles' % self.trust_id, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_admin_cannot_get_trust_role_for_other_user(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + ('/v3/OS-TRUST/trusts/%s/roles/%s' % + (self.trust_id, self.bootstrapper.member_role_id)), + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class AdminTokenTests(TrustTests, _AdminTestsMixin): + """Tests for the is_admin user. + + The Trusts API has hardcoded is_admin checks that we need to ensure are + preserved through the system-scope transition. + """ + + def setUp(self): + super(AdminTokenTests, self).setUp() + self.config_fixture.config(admin_token='ADMIN') + self.headers = {'X-Auth-Token': 'ADMIN'} + + def test_admin_can_delete_trust_for_other_user(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.delete( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.headers, + expected_status_code=http_client.NO_CONTENT + ) + + +class SystemAdminTests(TrustTests, _AdminTestsMixin): + """Tests for system admin users.""" + + def setUp(self): + super(SystemAdminTests, self).setUp() + # TODO(cmurphy) enable enforce_scope when trust policies become + # system-scope aware + # 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, + 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_admin_cannot_delete_trust_for_other_user(self): + # only the is_admin admin can do this + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.delete( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class ProjectUserTests(TrustTests): + """Tests for all project users.""" + + def setUp(self): + super(ProjectUserTests, self).setUp() + other_user = unit.new_user_ref(domain_id=self.domain_id) + self.other_user_id = PROVIDERS.identity_api.create_user( + other_user)['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=self.other_user_id, + project_id=self.project_id + ) + + auth = self.build_authentication_request( + user_id=self.other_user_id, + password=other_user['password'], + project_id=self.project_id + ) + # Grab a token using another persona who has no trusts associated with + # them + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.other_headers = {'X-Auth-Token': self.token_id} + + def test_user_can_list_trusts_of_whom_they_are_the_trustor(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + ('/v3/OS-TRUST/trusts?trustor_user_id=%s' % + self.trustor_user_id), + headers=self.trustor_headers + ) + self.assertEqual(1, len(r.json['trusts'])) + self.assertEqual(self.trust_id, r.json['trusts'][0]['id']) + + def test_user_can_list_trusts_delegated_to_them(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + ('/v3/OS-TRUST/trusts?trustee_user_id=%s' % + self.trustee_user_id), + headers=self.trustee_headers + ) + self.assertEqual(1, len(r.json['trusts'])) + self.assertEqual(self.trust_id, r.json['trusts'][0]['id']) + + def test_trustor_cannot_list_trusts_for_trustee(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + ('/v3/OS-TRUST/trusts?trustee_user_id=%s' % + self.trustee_user_id), + headers=self.trustor_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_trustee_cannot_list_trusts_for_trustor(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + ('/v3/OS-TRUST/trusts?trustor_user_id=%s' % + self.trustor_user_id), + headers=self.trustee_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_trusts_for_other_trustor(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + ('/v3/OS-TRUST/trusts?trustor_user_id=%s' % + self.trustor_user_id), + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_trusts_for_other_trustee(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + ('/v3/OS-TRUST/trusts?trustee_user_id=%s' % + self.trustee_user_id), + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_all_trusts(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts', + headers=self.trustee_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_get_another_users_trust(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_can_get_non_existent_trust_not_found(self): + trust_id = uuid.uuid4().hex + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s' % trust_id, + headers=self.other_headers, + expected_status_code=http_client.NOT_FOUND + ) + + def test_user_can_get_trust_of_whom_they_are_the_trustor(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.trustor_headers + ) + + def test_user_can_get_trust_delegated_to_them(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.trustee_headers + ) + self.assertEqual(r.json['trust']['id'], self.trust_id) + + def test_trustor_can_create_trust(self): + json = {'trust': self.trust_data['trust']} + json['trust']['roles'] = self.trust_data['roles'] + + with self.test_client() as c: + c.post( + '/v3/OS-TRUST/trusts', + json=json, + headers=self.trustor_headers + ) + + def test_trustee_cannot_create_trust(self): + json = {'trust': self.trust_data['trust']} + json['trust']['roles'] = self.trust_data['roles'] + + with self.test_client() as c: + c.post( + '/v3/OS-TRUST/trusts', + json=json, + headers=self.trustee_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_trustor_can_delete_trust(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.delete( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.trustor_headers + ) + + def test_trustee_cannot_delete_trust(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.delete( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.trustee_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_trust_for_other_user(self): + ref = PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.delete( + '/v3/OS-TRUST/trusts/%s' % ref['id'], + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_trustor_can_list_trust_roles(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + '/v3/OS-TRUST/trusts/%s/roles' % self.trust_id, + headers=self.trustor_headers + ) + self.assertEqual(r.json['roles'][0]['id'], + self.bootstrapper.member_role_id) + + def test_trustee_can_list_trust_roles(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + r = c.get( + '/v3/OS-TRUST/trusts/%s/roles' % self.trust_id, + headers=self.trustee_headers + ) + self.assertEqual(r.json['roles'][0]['id'], + self.bootstrapper.member_role_id) + + def test_user_cannot_list_trust_roles_for_other_user(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.get( + '/v3/OS-TRUST/trusts/%s/roles' % self.trust_id, + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_trustor_can_get_trust_role(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.head( + ('/v3/OS-TRUST/trusts/%s/roles/%s' % + (self.trust_id, self.bootstrapper.member_role_id)), + headers=self.trustor_headers + ) + + def test_trustee_can_get_trust_role(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.head( + ('/v3/OS-TRUST/trusts/%s/roles/%s' % + (self.trust_id, self.bootstrapper.member_role_id)), + headers=self.trustee_headers + ) + + def test_user_cannot_get_trust_role_for_other_user(self): + PROVIDERS.trust_api.create_trust( + self.trust_id, **self.trust_data) + + with self.test_client() as c: + c.head( + ('/v3/OS-TRUST/trusts/%s/roles/%s' % + (self.trust_id, self.bootstrapper.member_role_id)), + headers=self.other_headers, + expected_status_code=http_client.FORBIDDEN + )