From 239bed09a922d6076711ca5c112be6299fa0f0bb Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Tue, 21 Aug 2018 20:41:38 +0000 Subject: [PATCH] Implement scope_type checking for credentials This change adds tests cases for the default roles keystone supports at install time. It also modifies the policies for the credentials API to be more self-service by properly checking for various scopes. Closes-Bug: 1788415 Partial-Bug: 968696 Change-Id: Ifedb7798c96930b6cc0f91159a14a21ac4b02f9f --- keystone/api/credentials.py | 61 +- keystone/common/policies/credential.py | 103 +- keystone/tests/unit/base_classes.py | 1 + keystone/tests/unit/protection/__init__.py | 0 keystone/tests/unit/protection/v3/__init__.py | 0 .../unit/protection/v3/test_credentials.py | 1137 +++++++++++++++++ keystone/tests/unit/test_v3_credential.py | 18 +- .../notes/bug-1788415-3190279e9c900f76.yaml | 25 + 8 files changed, 1309 insertions(+), 36 deletions(-) create mode 100644 keystone/tests/unit/protection/__init__.py create mode 100644 keystone/tests/unit/protection/v3/__init__.py create mode 100644 keystone/tests/unit/protection/v3/test_credentials.py create mode 100644 releasenotes/notes/bug-1788415-3190279e9c900f76.yaml diff --git a/keystone/api/credentials.py b/keystone/api/credentials.py index 8f6fda38ca..f258e19de4 100644 --- a/keystone/api/credentials.py +++ b/keystone/api/credentials.py @@ -21,16 +21,31 @@ from six.moves import http_client from keystone.common import provider_api from keystone.common import rbac_enforcer from keystone.common import validation +import keystone.conf from keystone.credential import schema from keystone import exception from keystone.i18n import _ from keystone.server import flask as ks_flask - +CONF = keystone.conf.CONF PROVIDERS = provider_api.ProviderAPIs ENFORCER = rbac_enforcer.RBACEnforcer +def _build_target_enforcement(): + target = {} + try: + target['credential'] = PROVIDERS.credential_api.get_credential( + flask.request.view_args.get('credential_id') + ) + except exception.NotFound: # nosec + # Defer existance in the event the credential doesn't exist, we'll + # check this later anyway. + pass + + return target + + class CredentialResource(ks_flask.ResourceBase): collection_key = 'credentials' member_key = 'credential' @@ -75,17 +90,34 @@ class CredentialResource(ks_flask.ResourceBase): def _list_credentials(self): filters = ['user_id', 'type'] + if not self.oslo_context.system_scope: + target = {'credential': {'user_id': self.oslo_context.user_id}} + else: + target = None ENFORCER.enforce_call(action='identity:list_credentials', - filters=filters) + filters=filters, target_attr=target) hints = self.build_driver_hints(filters) refs = PROVIDERS.credential_api.list_credentials(hints) + # If the request was filtered, make sure to return only the + # credentials specific to that user. This makes it so that users with + # roles on projects can't see credentials that aren't theirs. + if (not self.oslo_context.system_scope and + CONF.oslo_policy.enforce_scope): + filtered_refs = [] + for ref in refs: + if ref['user_id'] == target['credential']['user_id']: + filtered_refs.append(ref) + refs = filtered_refs refs = [self._blob_to_json(r) for r in refs] return self.wrap_collection(refs, hints=hints) def _get_credential(self, credential_id): - ENFORCER.enforce_call(action='identity:get_credential') - ref = PROVIDERS.credential_api.get_credential(credential_id) - return self.wrap_member(self._blob_to_json(ref)) + ENFORCER.enforce_call( + action='identity:get_credential', + build_target=_build_target_enforcement + ) + credential = PROVIDERS.credential_api.get_credential(credential_id) + return self.wrap_member(self._blob_to_json(credential)) def get(self, credential_id=None): # Get Credential or List of credentials. @@ -97,8 +129,12 @@ class CredentialResource(ks_flask.ResourceBase): def post(self): # Create a new credential - ENFORCER.enforce_call(action='identity:create_credential') credential = flask.request.json.get('credential', {}) + target = {} + target['credential'] = credential + ENFORCER.enforce_call( + action='identity:create_credential', target_attr=target + ) validation.lazy_validate(schema.credential_create, credential) trust_id = getattr(self.oslo_context, 'trust_id', None) ref = self._assign_unique_id( @@ -108,7 +144,12 @@ class CredentialResource(ks_flask.ResourceBase): def patch(self, credential_id): # Update Credential - ENFORCER.enforce_call(action='identity:update_credential') + ENFORCER.enforce_call( + action='identity:update_credential', + build_target=_build_target_enforcement + ) + PROVIDERS.credential_api.get_credential(credential_id) + credential = flask.request.json.get('credential', {}) validation.lazy_validate(schema.credential_update, credential) self._require_matching_id(credential) @@ -118,7 +159,11 @@ class CredentialResource(ks_flask.ResourceBase): def delete(self, credential_id): # Delete credentials - ENFORCER.enforce_call(action='identity:delete_credential') + ENFORCER.enforce_call( + action='identity:delete_credential', + build_target=_build_target_enforcement + ) + return (PROVIDERS.credential_api.delete_credential(credential_id), http_client.NO_CONTENT) diff --git a/keystone/common/policies/credential.py b/keystone/common/policies/credential.py index 147f31c901..f04ba5438d 100644 --- a/keystone/common/policies/credential.py +++ b/keystone/common/policies/credential.py @@ -10,56 +10,109 @@ # 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 +SYSTEM_READER_OR_CRED_OWNER = ( + '(role:reader and system_scope:all) ' + 'or user_id:%(target.credential.user_id)s' +) +SYSTEM_MEMBER_OR_CRED_OWNER = ( + '(role:member and system_scope:all) ' + 'or user_id:%(target.credential.user_id)s' +) +SYSTEM_ADMIN_OR_CRED_OWNER = ( + '(role:admin and system_scope:all) ' + 'or user_id:%(target.credential.user_id)s' +) + +DEPRECATED_REASON = ( + 'As of the Stein release, the credential API now understands how to ' + 'handle system-scoped tokens in addition to project-scoped 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.' +) +deprecated_get_credential = policy.DeprecatedRule( + name=base.IDENTITY % 'get_credential', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_list_credentials = policy.DeprecatedRule( + name=base.IDENTITY % 'list_credentials', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_create_credential = policy.DeprecatedRule( + name=base.IDENTITY % 'create_credential', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_update_credential = policy.DeprecatedRule( + name=base.IDENTITY % 'update_credential', + check_str=base.RULE_ADMIN_REQUIRED +) +deprecated_delete_credential = policy.DeprecatedRule( + name=base.IDENTITY % 'delete_credential', + check_str=base.RULE_ADMIN_REQUIRED +) + + credential_policies = [ policy.DocumentedRuleDefault( name=base.IDENTITY % 'get_credential', - check_str=base.RULE_ADMIN_REQUIRED, - # FIXME(lbragstad): Credentials aren't really project-scoped or - # system-scoped. Instead, they are tied to a user. If this API is - # called with a system-scoped token, it's a system-administrator and - # they should be able to get any credential for management reasons. If - # this API is called with a project-scoped token, then extra - # enforcement needs to happen based on who created the credential, what - # projects they are members of, and the project the token is scoped to. - # When we fully support the second case, we can add `project` to the - # list of scope_types. This comment applies to the rest of the policies - # in this module. - # scope_types=['system', 'project'], + check_str=SYSTEM_READER_OR_CRED_OWNER, + scope_types=['system', 'project'], description='Show credentials details.', operations=[{'path': '/v3/credentials/{credential_id}', - 'method': 'GET'}]), + 'method': 'GET'}], + deprecated_rule=deprecated_get_credential, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ), policy.DocumentedRuleDefault( name=base.IDENTITY % 'list_credentials', - check_str=base.RULE_ADMIN_REQUIRED, - # scope_types=['system', 'project'], + check_str=SYSTEM_READER_OR_CRED_OWNER, + scope_types=['system', 'project'], description='List credentials.', operations=[{'path': '/v3/credentials', - 'method': 'GET'}]), + 'method': 'GET'}], + deprecated_rule=deprecated_list_credentials, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ), policy.DocumentedRuleDefault( name=base.IDENTITY % 'create_credential', - check_str=base.RULE_ADMIN_REQUIRED, - # scope_types=['system', 'project'], + check_str=SYSTEM_ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], description='Create credential.', operations=[{'path': '/v3/credentials', - 'method': 'POST'}]), + 'method': 'POST'}], + deprecated_rule=deprecated_create_credential, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ), policy.DocumentedRuleDefault( name=base.IDENTITY % 'update_credential', - check_str=base.RULE_ADMIN_REQUIRED, - # scope_types=['system', 'project'], + check_str=SYSTEM_MEMBER_OR_CRED_OWNER, + scope_types=['system', 'project'], description='Update credential.', operations=[{'path': '/v3/credentials/{credential_id}', - 'method': 'PATCH'}]), + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_credential, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ), policy.DocumentedRuleDefault( name=base.IDENTITY % 'delete_credential', - check_str=base.RULE_ADMIN_REQUIRED, - # scope_types=['system', 'project'], + check_str=SYSTEM_ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], description='Delete credential.', operations=[{'path': '/v3/credentials/{credential_id}', - 'method': 'DELETE'}]) + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_credential, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ) ] diff --git a/keystone/tests/unit/base_classes.py b/keystone/tests/unit/base_classes.py index fb6e3af9b2..39001d750e 100644 --- a/keystone/tests/unit/base_classes.py +++ b/keystone/tests/unit/base_classes.py @@ -41,6 +41,7 @@ class TestCaseWithBootstrap(core.BaseTestCase): self.useFixture(database.Database()) super(TestCaseWithBootstrap, self).setUp() self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + CONF(args=[], project='keystone') self.useFixture( ksfixtures.KeyRepository( self.config_fixture, diff --git a/keystone/tests/unit/protection/__init__.py b/keystone/tests/unit/protection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/tests/unit/protection/v3/__init__.py b/keystone/tests/unit/protection/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/tests/unit/protection/v3/test_credentials.py b/keystone/tests/unit/protection/v3/test_credentials.py new file mode 100644 index 0000000000..21a26519ef --- /dev/null +++ b/keystone/tests/unit/protection/v3/test_credentials.py @@ -0,0 +1,1137 @@ +# 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 oslo_serialization import jsonutils +from six.moves import http_client + +from keystone.common.policies import credential as cp +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 + + +class _UserCredentialTests(object): + """Test cases for anyone that has a valid user token.""" + + def test_user_can_create_credentials_for_themselves(self): + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'user_id': self.user_id, + 'type': uuid.uuid4().hex + } + } + with self.test_client() as c: + c.post('/v3/credentials', json=create, headers=self.headers) + + def test_user_can_get_their_credentials(self): + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': self.user_id + } + } + r = c.post('/v3/credentials', json=create, headers=self.headers) + credential_id = r.json['credential']['id'] + + path = '/v3/credentials/%s' % credential_id + r = c.get(path, headers=self.headers) + self.assertEqual( + self.user_id, r.json['credential']['user_id'] + ) + + def test_user_can_list_their_credentials(self): + with self.test_client() as c: + expected = [] + for _ in range(2): + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': self.user_id + } + } + r = c.post( + '/v3/credentials', json=create, headers=self.headers + ) + expected.append(r.json['credential']) + + r = c.get('/v3/credentials', headers=self.headers) + for credential in expected: + self.assertIn(credential, r.json['credentials']) + + def test_user_can_filter_their_credentials_by_type_and_user(self): + with self.test_client() as c: + credential_type = uuid.uuid4().hex + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': credential_type, + 'user_id': self.user_id + } + } + r = c.post( + '/v3/credentials', json=create, headers=self.headers + ) + expected_credential_id = r.json['credential']['id'] + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': self.user_id + } + } + r = c.post( + '/v3/credentials', json=create, headers=self.headers + ) + + path = '/v3/credentials?type=%s' % credential_type + r = c.get(path, headers=self.headers) + self.assertEqual( + expected_credential_id, r.json['credentials'][0]['id'] + ) + + path = '/v3/credentials?user=%s' % self.user_id + r = c.get(path, headers=self.headers) + self.assertEqual( + expected_credential_id, r.json['credentials'][0]['id'] + ) + + def test_user_can_update_their_credential(self): + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': self.user_id + } + } + + r = c.post('/v3/credentials', json=create, headers=self.headers) + credential_id = r.json['credential']['id'] + + updated_blob = uuid.uuid4().hex + update = {'credential': {'blob': updated_blob}} + path = '/v3/credentials/%s' % credential_id + r = c.patch(path, json=update, headers=self.headers) + self.assertEqual(updated_blob, r.json['credential']['blob']) + + def test_user_can_delete_their_credentials(self): + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': self.user_id + } + } + r = c.post('/v3/credentials', json=create, headers=self.headers) + credential_id = r.json['credential']['id'] + + path = '/v3/credentials/%s' % credential_id + c.delete(path, headers=self.headers) + + +class _ProjectUsersTests(object): + """Users who have project role authorization observe the same behavior.""" + + def test_user_cannot_get_credentials_for_other_users(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + c.get( + path, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_get_non_existant_credential_forbidden(self): + with self.test_client() as c: + c.get( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_list_credentials_for_other_users(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post('/v3/credentials', json=create, headers=headers) + + with self.test_client() as c: + path = '/v3/credentials?user_id=%s' % user['id'] + r = c.get(path, headers=self.headers) + self.assertEqual([], r.json['credentials']) + + def test_user_cannot_filter_credentials_by_type_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + credential_type = uuid.uuid4().hex + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': credential_type, + 'user_id': user['id'] + } + } + c.post('/v3/credentials', json=create, headers=headers) + + with self.test_client() as c: + path = '/v3/credentials?type=%s' % credential_type + r = c.get(path, headers=self.headers) + self.assertEqual(0, len(r.json['credentials'])) + + def test_user_cannot_filter_credentials_by_user_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + expected_cred_ids = [] + for _ in range(2): + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + expected_cred_ids.append(r.json['credential']['id']) + + with self.test_client() as c: + path = '/v3/credentials?user_id=%s' % user['id'] + r = c.get(path, headers=self.headers) + self.assertEqual([], r.json['credentials']) + + def test_user_cannot_update_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + path = '/v3/credentials/%s' % credential_id + c.patch( + path, json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_non_existant_credential_forbidden(self): + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + + c.patch( + '/v3/credentials/%s' % uuid.uuid4().hex, json=update, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_create_credentials_for_other_users(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post( + '/v3/credentials', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + c.delete( + path, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_non_existant_credential_forbidden(self): + with self.test_client() as c: + c.delete( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class _SystemUserCredentialTests(object): + """Tests that are common across all system users.""" + + def test_user_can_list_credentials_for_other_users(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + r = c.get('/v3/credentials', headers=self.headers) + self.assertEqual(1, len(r.json['credentials'])) + self.assertEqual(credential_id, r.json['credentials'][0]['id']) + self.assertEqual(user['id'], r.json['credentials'][0]['user_id']) + + def test_user_cannot_get_non_existant_credential_not_found(self): + with self.test_client() as c: + c.get( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.NOT_FOUND + ) + + def test_user_can_filter_credentials_by_type_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + credential_type = uuid.uuid4().hex + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': credential_type, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post('/v3/credentials', json=create, headers=headers) + + with self.test_client() as c: + path = '/v3/credentials?type=%s' % credential_type + r = c.get(path, headers=self.headers) + self.assertEqual(1, len(r.json['credentials'])) + self.assertEqual(credential_id, r.json['credentials'][0]['id']) + self.assertEqual(user['id'], r.json['credentials'][0]['user_id']) + + def test_user_can_filter_credentials_by_user_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + expected_cred_ids = [] + for _ in range(2): + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + expected_cred_ids.append(r.json['credential']['id']) + + with self.test_client() as c: + path = '/v3/credentials?user_id=%s' % user['id'] + r = c.get(path, headers=self.headers) + self.assertEqual(2, len(r.json['credentials'])) + for credential in r.json['credentials']: + self.assertIn(credential['id'], expected_cred_ids) + self.assertEqual(user['id'], credential['user_id']) + + +class SystemReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _UserCredentialTests, + _SystemUserCredentialTests): + + 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} + + def test_user_cannot_create_credentials_for_other_users(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post( + '/v3/credentials', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + path = '/v3/credentials/%s' % credential_id + c.patch( + path, json=update, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_update_non_existant_credential_forbidden(self): + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + + c.patch( + '/v3/credentials/%s' % uuid.uuid4().hex, json=update, + headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + c.delete( + path, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_non_existant_credential_forbidden(self): + with self.test_client() as c: + c.delete( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class SystemMemberTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _UserCredentialTests, + _SystemUserCredentialTests): + + 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} + + def test_user_cannot_create_credentials_for_other_users(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post( + '/v3/credentials', json=create, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_can_update_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + path = '/v3/credentials/%s' % credential_id + c.patch(path, json=update, headers=self.headers) + + def test_user_cannot_update_non_existant_credential_not_found(self): + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + + c.patch( + '/v3/credentials/%s' % uuid.uuid4().hex, json=update, + headers=self.headers, + expected_status_code=http_client.NOT_FOUND + ) + + def test_user_cannot_delete_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + c.delete( + path, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_delete_non_existant_credential_forbidden(self): + with self.test_client() as c: + c.delete( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class SystemAdminTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _UserCredentialTests, + _SystemUserCredentialTests): + + 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_credentials_for_other_users(self): + user = PROVIDERS.identity_api.create_user( + unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + ) + + with self.test_client() as c: + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + c.post('/v3/credentials', json=create, headers=self.headers) + + def test_user_can_update_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + updated_blob = uuid.uuid4().hex + update = {'credential': {'blob': updated_blob}} + r = c.patch(path, json=update, headers=self.headers) + self.assertEqual(updated_blob, r.json['credential']['blob']) + self.assertEqual(user['id'], r.json['credential']['user_id']) + + def test_user_cannot_update_non_existant_credential_not_found(self): + with self.test_client() as c: + update = {'credential': {'blob': uuid.uuid4().hex}} + + c.patch( + '/v3/credentials/%s' % uuid.uuid4().hex, json=update, + headers=self.headers, + expected_status_code=http_client.NOT_FOUND + ) + + def test_user_can_delete_credentials_for_others(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_password = user['password'] + user = PROVIDERS.identity_api.create_user(user) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + project = PROVIDERS.resource_api.create_project(project['id'], project) + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=user['id'], + project_id=project['id'] + ) + user_auth = self.build_authentication_request( + user_id=user['id'], password=user_password, + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=user_auth) + token_id = r.headers['X-Subject-Token'] + headers = {'X-Auth-Token': token_id} + + create = { + 'credential': { + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'user_id': user['id'] + } + } + r = c.post('/v3/credentials', json=create, headers=headers) + credential_id = r.json['credential']['id'] + + with self.test_client() as c: + path = '/v3/credentials/%s' % credential_id + c.delete(path, headers=self.headers) + + def test_user_cannot_delete_non_existant_credential_not_found(self): + with self.test_client() as c: + c.delete( + '/v3/credentials/%s' % uuid.uuid4().hex, headers=self.headers, + expected_status_code=http_client.NOT_FOUND + ) + + +class ProjectReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _UserCredentialTests, + _ProjectUsersTests): + + 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) + + project_reader = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + project_reader + )['id'] + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + self.project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['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, + _UserCredentialTests, + _ProjectUsersTests): + + 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) + + project_member = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + project_member + )['id'] + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id + ) + self.project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['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, + _UserCredentialTests, + _ProjectUsersTests): + + 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) + + # 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, + 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.credentials 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_credential': cp.SYSTEM_READER_OR_CRED_OWNER, + 'identity:list_credentials': cp.SYSTEM_READER_OR_CRED_OWNER, + 'identity:create_credential': cp.SYSTEM_ADMIN_OR_CRED_OWNER, + 'identity:update_credential': cp.SYSTEM_MEMBER_OR_CRED_OWNER, + 'identity:delete_credential': cp.SYSTEM_ADMIN_OR_CRED_OWNER + } + f.write(jsonutils.dumps(overridden_policies)) diff --git a/keystone/tests/unit/test_v3_credential.py b/keystone/tests/unit/test_v3_credential.py index 1907c2093d..b5e08249ac 100644 --- a/keystone/tests/unit/test_v3_credential.py +++ b/keystone/tests/unit/test_v3_credential.py @@ -113,6 +113,11 @@ class CredentialTestCase(CredentialBaseTestCase): def test_list_credentials_filtered_by_type(self): """Call ``GET /credentials?type={type}``.""" + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.role_id + ) + token = self.get_system_scoped_token() + # The type ec2 was chosen, instead of a random string, # because the type must be in the list of supported types ec2_credential = unit.new_credential_ref(user_id=uuid.uuid4().hex, @@ -123,14 +128,14 @@ class CredentialTestCase(CredentialBaseTestCase): ec2_credential['id'], ec2_credential) # The type cert was chosen for the same reason as ec2 - r = self.get('/credentials?type=cert') + r = self.get('/credentials?type=cert', token=token) # Testing the filter for two different types self.assertValidCredentialListResponse(r, ref=self.credential) for cred in r.result['credentials']: self.assertEqual('cert', cred['type']) - r_ec2 = self.get('/credentials?type=ec2') + r_ec2 = self.get('/credentials?type=ec2', token=token) self.assertThat(r_ec2.result['credentials'], matchers.HasLength(1)) cred_ec2 = r_ec2.result['credentials'][0] @@ -143,6 +148,11 @@ class CredentialTestCase(CredentialBaseTestCase): user1_id = uuid.uuid4().hex user2_id = uuid.uuid4().hex + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.role_id + ) + token = self.get_system_scoped_token() + # Creating credentials for two different users credential_user1_ec2 = unit.new_credential_ref(user_id=user1_id, type=CRED_TYPE_EC2) @@ -156,7 +166,9 @@ class CredentialTestCase(CredentialBaseTestCase): PROVIDERS.credential_api.create_credential( credential_user2_cert['id'], credential_user2_cert) - r = self.get('/credentials?user_id=%s&type=ec2' % user1_id) + r = self.get( + '/credentials?user_id=%s&type=ec2' % user1_id, token=token + ) self.assertValidCredentialListResponse(r, ref=credential_user1_ec2) self.assertThat(r.result['credentials'], matchers.HasLength(1)) cred = r.result['credentials'][0] diff --git a/releasenotes/notes/bug-1788415-3190279e9c900f76.yaml b/releasenotes/notes/bug-1788415-3190279e9c900f76.yaml new file mode 100644 index 0000000000..9f9f2a92bb --- /dev/null +++ b/releasenotes/notes/bug-1788415-3190279e9c900f76.yaml @@ -0,0 +1,25 @@ +--- +upgrade: + - | + [`bug 1788415 `_] + [`bug 968696 `_] + Policies protecting the ``/v3/credentials`` API have changed defaults in + order to make the credentials API more accessible for all users and not + just operators or system administrator. Please consider these updates when + using this version of keystone since it could affect API behavior in your + deployment, especially if you're using a customized policy file. +security: + - | + [`bug 1788415 `_] + [`bug 968696 `_] + More granular policy checks have been applied to the credential API in + order to make it more self-service for users. By default, end users will + now have the ability to manage their credentials. +fixes: + - | + [`bug 1788415 `_] + [`bug 968696 `_] + Improved self-service support has been implemented in the credential API. + This means that end users have the ability to manage their own credentials + as opposed to filing tickets to have deployment administrators manage + credentials for users.