diff --git a/keystone/contrib/ec2/controllers.py b/keystone/contrib/ec2/controllers.py index 390d45fe52..b54c0886cc 100644 --- a/keystone/contrib/ec2/controllers.py +++ b/keystone/contrib/ec2/controllers.py @@ -59,7 +59,7 @@ CONF = keystone.conf.CONF @dependency.requires('assignment_api', 'catalog_api', 'credential_api', 'identity_api', 'resource_api', 'role_api', - 'token_provider_api') + 'token_provider_api', 'trust_api', 'oauth_api') @six.add_metaclass(abc.ABCMeta) class Ec2ControllerCommon(object): def check_signature(self, creds_ref, credentials): @@ -143,7 +143,8 @@ class Ec2ControllerCommon(object): def _authenticate(self, credentials=None, ec2credentials=None): """Common code shared between the V2 and V3 authenticate methods. - :returns: user_ref, tenant_ref, roles_ref, catalog_ref + :returns: user_ref, tenant_ref, roles_ref, catalog_ref, trust_ref, + auth_context """ # FIXME(ja): validate that a service token was used! @@ -176,9 +177,31 @@ class Ec2ControllerCommon(object): sys.exc_info()[2]) self._check_timestamp(credentials) - roles = self.assignment_api.get_roles_for_user_and_project( - user_ref['id'], tenant_ref['id'] - ) + + trustee_user_id = None + auth_context = None + trust_ref = {} + if creds_ref['trust_id']: + trust_ref = self.trust_api.get_trust(creds_ref['trust_id']) + roles = [r['id'] for r in trust_ref['roles']] + # NOTE(cmurphy): if this credential was created using a + # trust-scoped token with impersonation, the user_id will be for + # the trustor, not the trustee. In this case, issuing a + # trust-scoped token to the trustor will fail. In order to get a + # trust-scoped token, use the user ID of the trustee. With + # impersonation, the resulting token will still be for the trustor. + # Without impersonation, the token will be for the trustee. + if trust_ref['impersonation'] is True: + trustee_user_id = trust_ref['trustee_user_id'] + user_ref = self.identity_api.get_user(trustee_user_id) + elif creds_ref['access_token_id']: + access_token = self.oauth_api.get_access_token( + creds_ref['access_token_id']) + roles = jsonutils.loads(access_token['role_ids']) + auth_context = {'access_token_id': creds_ref['access_token_id']} + else: + roles = self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], tenant_ref['id']) if not roles: raise exception.Unauthorized( message=_('User not valid for tenant.')) @@ -187,7 +210,8 @@ class Ec2ControllerCommon(object): catalog_ref = self.catalog_api.get_catalog( user_ref['id'], tenant_ref['id']) - return user_ref, tenant_ref, roles_ref, catalog_ref + return (user_ref, tenant_ref, roles_ref, catalog_ref, trust_ref, + auth_context) def create_credential(self, request, user_id, tenant_id): """Create a secret/access pair for use with ec2 style auth. @@ -267,7 +291,8 @@ class Ec2ControllerCommon(object): 'tenant_id': credential.get('project_id'), 'access': blob.get('access'), 'secret': blob.get('secret'), - 'trust_id': blob.get('trust_id')} + 'trust_id': blob.get('trust_id'), + 'access_token_id': blob.get('access_token_id')} def _get_credentials(self, credential_id): """Return credentials from an ID. @@ -304,7 +329,8 @@ class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): @controller.v2_ec2_deprecated def authenticate(self, request, credentials=None, ec2Credentials=None): - (user_ref, project_ref, roles_ref, catalog_ref) = self._authenticate( + (user_ref, project_ref, roles_ref, catalog_ref, + trust_ref, auth_context) = self._authenticate( credentials=credentials, ec2credentials=ec2Credentials ) @@ -410,14 +436,16 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): self.check_protection(request, prep_info, ref) def authenticate(self, context, credentials=None, ec2Credentials=None): - (user_ref, project_ref, roles_ref, catalog_ref) = self._authenticate( - credentials=credentials, ec2credentials=ec2Credentials - ) + (user_ref, project_ref, roles_ref, catalog_ref, trust_ref, + auth_context) = (self._authenticate( + credentials=credentials, ec2credentials=ec2Credentials)) method_names = ['ec2credential'] token_id, token_data = self.token_provider_api.issue_token( - user_ref['id'], method_names, project_id=project_ref['id']) + user_ref['id'], method_names, project_id=project_ref['id'], + trust=trust_ref, auth_context=auth_context + ) return self.render_token_data_response(token_id, token_data) @controller.protected(callback=_check_credential_owner_and_user_id_match) diff --git a/keystone/credential/controllers.py b/keystone/credential/controllers.py index 867694e319..3dcf492d9c 100644 --- a/keystone/credential/controllers.py +++ b/keystone/credential/controllers.py @@ -13,6 +13,7 @@ # under the License. import hashlib +import six from oslo_serialization import jsonutils @@ -33,30 +34,38 @@ class CredentialV3(controller.V3Controller): super(CredentialV3, self).__init__() self.get_member_from_driver = self.credential_api.get_credential - def _assign_unique_id(self, ref, trust_id=None): - # Generates and assigns a unique identifier to - # a credential reference. + def _validate_blob_json(self, ref): + try: + blob = jsonutils.loads(ref.get('blob')) + except (ValueError, TabError): + raise exception.ValidationError( + message=_('Invalid blob in credential')) + if not blob or not isinstance(blob, dict): + raise exception.ValidationError(attribute='blob', + target='credential') + if blob.get('access') is None: + raise exception.ValidationError(attribute='access', + target='credential') + return blob + + def _assign_unique_id( + self, ref, trust_id=None, access_token_id=None): + # Generates and assigns a unique identifier to a credential reference. if ref.get('type', '').lower() == 'ec2': - try: - blob = jsonutils.loads(ref.get('blob')) - except (ValueError, TypeError): - raise exception.ValidationError( - message=_('Invalid blob in credential')) - if not blob or not isinstance(blob, dict): - raise exception.ValidationError(attribute='blob', - target='credential') - if blob.get('access') is None: - raise exception.ValidationError(attribute='access', - target='blob') + blob = self._validate_blob_json(ref) ret_ref = ref.copy() ret_ref['id'] = hashlib.sha256( blob['access'].encode('utf8')).hexdigest() - # Update the blob with the trust_id, so credentials created - # with a trust scoped token will result in trust scoped - # tokens when authentication via ec2tokens happens + # update the blob with the trust_id, so credentials + # created with a trust- token will result in + # trust- cred-scoped tokens when authentication via + # ec2tokens happens if trust_id is not None: blob['trust_id'] = trust_id ret_ref['blob'] = jsonutils.dumps(blob) + if access_token_id is not None: + blob['access_token_id'] = access_token_id + ret_ref['blob'] = jsonutils.dumps(blob) return ret_ref else: return super(CredentialV3, self)._assign_unique_id(ref) @@ -64,8 +73,11 @@ class CredentialV3(controller.V3Controller): @controller.protected() def create_credential(self, request, credential): validation.lazy_validate(schema.credential_create, credential) + trust_id = request.context.trust_id + access_token_id = request.context.oauth_acess_token_id ref = self._assign_unique_id(self._normalize_dict(credential), - request.context.trust_id) + trust_id=trust_id, + access_token_id=access_token_id) ref = self.credential_api.create_credential(ref['id'], ref) return CredentialV3.wrap_member(request.context_dict, ref) @@ -95,11 +107,31 @@ class CredentialV3(controller.V3Controller): ret_ref = self._blob_to_json(ref) return CredentialV3.wrap_member(request.context_dict, ret_ref) + def _validate_blob_update_keys(self, credential, ref): + if credential.get('type', '').lower() == 'ec2': + new_blob = self._validate_blob_json(ref) + old_blob = credential.get('blob') + if isinstance(old_blob, six.string_types): + old_blob = jsonutils.loads(old_blob) + # if there was a scope set, prevent changing it or unsetting it + for key in ['trust_id', 'app_cred_id', 'access_token_id']: + if old_blob.get(key) != new_blob.get(key): + message = _('%s can not be updated for credential') % key + raise exception.ValidationError(message=message) + @controller.protected() def update_credential(self, request, credential_id, credential): + current = self.credential_api.get_credential(credential_id) validation.lazy_validate(schema.credential_update, credential) + self._validate_blob_update_keys(current.copy(), credential.copy()) self._require_matching_id(credential_id, credential) - + # Check that the user hasn't illegally modified the owner or scope + target = {'credential': dict(current, **credential)} + prep_info = {'f_name': 'update_credential', + 'input_attr': {}} + self.check_protection( + request, prep_info, target_attr=target + ) ref = self.credential_api.update_credential(credential_id, credential) return CredentialV3.wrap_member(request.context_dict, ref) diff --git a/keystone/tests/unit/test_v3_credential.py b/keystone/tests/unit/test_v3_credential.py index ebfcbffd6a..bf113e7be3 100644 --- a/keystone/tests/unit/test_v3_credential.py +++ b/keystone/tests/unit/test_v3_credential.py @@ -17,15 +17,17 @@ import json import uuid from keystoneclient.contrib.ec2 import utils as ec2_utils -from six.moves import http_client +from six.moves import http_client, urllib from testtools import matchers from keystone.common import utils from keystone.contrib.ec2 import controllers from keystone.credential.providers import fernet as credential_fernet from keystone import exception +from keystone import oauth1 from keystone.tests import unit from keystone.tests.unit import ksfixtures +from keystone.tests.unit.ksfixtures import temporaryfile from keystone.tests.unit import test_v3 @@ -59,6 +61,33 @@ class CredentialBaseTestCase(test_v3.RestfulTestCase): return json.dumps(blob), credential_id + def _test_get_token(self, access, secret): + """Test signature validation with the access/secret provided.""" + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + # Now make a request to validate the signed dummy request via the + # ec2tokens API. This proves the v3 ec2 credentials actually work. + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + r = self.post( + '/ec2tokens', + body={'ec2Credentials': sig_ref}, + expected_status=http_client.OK) + self.assertValidTokenResponse(r) + return r.result['token'] + class CredentialTestCase(CredentialBaseTestCase): """Test credential CRUD.""" @@ -238,6 +267,90 @@ class CredentialTestCase(CredentialBaseTestCase): 'credential_id': credential_id}, body={'credential': update_ref}) + def test_update_ec2_credential_change_trust_id(self): + """Call ``PATCH /credentials/{credential_id}``.""" + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + blob['trust_id'] = uuid.uuid4().hex + ref['blob'] = json.dumps(blob) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + credential_id = r.result.get('credential')['id'] + # Try changing to a different trust + blob['trust_id'] = uuid.uuid4().hex + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + # Try removing the trust + del blob['trust_id'] + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + + def test_update_ec2_credential_change_app_cred_id(self): + """Call ``PATCH /credentials/{credential_id}``.""" + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + blob['app_cred_id'] = uuid.uuid4().hex + ref['blob'] = json.dumps(blob) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + credential_id = r.result.get('credential')['id'] + # Try changing to a different app cred + blob['app_cred_id'] = uuid.uuid4().hex + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + # Try removing the app cred + del blob['app_cred_id'] + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + + def test_update_ec2_credential_change_access_token_id(self): + """Call ``PATCH /credentials/{credential_id}``.""" + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + blob['access_token_id'] = uuid.uuid4().hex + ref['blob'] = json.dumps(blob) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + credential_id = r.result.get('credential')['id'] + # Try changing to a different access token + blob['access_token_id'] = uuid.uuid4().hex + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + # Try removing the access token + del blob['access_token_id'] + update_ref = {'blob': json.dumps(blob)} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + body={'credential': update_ref}, + expected_status=http_client.BAD_REQUEST) + def test_delete_credential(self): """Call ``DELETE /credentials/{credential_id}``.""" self.delete( @@ -341,7 +454,132 @@ class CredentialTestCase(CredentialBaseTestCase): self.assertValidCredentialResponse(r, ref) -class TestCredentialTrustScoped(test_v3.RestfulTestCase): +class CredentialSelfServiceTestCase(CredentialBaseTestCase): + """Test self-service credential CRUD.""" + + def _policy_fixture(self): + return ksfixtures.Policy(self.tmpfilename, self.config_fixture) + + def _set_policy(self, new_policy): + with open(self.tmpfilename, "w") as policyfile: + policyfile.write(json.dumps(new_policy)) + + def setUp(self): + self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) + self.tmpfilename = self.tempfile.file_name + super(CredentialSelfServiceTestCase, self).setUp() + + # set the self-service credential policies + self_service_credential_policies = { + "identity:create_credential": "user_id:%(credential.user_id)s", + "identity:list_credentials": "user_id:%(user_id)s", + "identity:get_credential": "user_id:%(target.credential.user_id)s", + "identity:update_credential": + "user_id:%(target.credential.user_id)s", + "identity:delete_credential": + "user_id:%(target.credential.user_id)s" + } + self._set_policy(self_service_credential_policies) + + # remove the 'admin' role from user and replace it with an + # arbitrary role + self.assignment_api.remove_role_from_user_and_project( + self.user_id, self.project_id, self.role_id) + self.arbitrary_role = unit.new_role_ref(name=uuid.uuid4().hex) + self.role_api.create_role(self.arbitrary_role['id'], + self.arbitrary_role) + self.assignment_api.add_role_to_user_and_project( + self.user_id, self.project_id, self.arbitrary_role['id']) + + self.credential = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + + self.credential_api.create_credential( + self.credential['id'], + self.credential) + + def test_list_credentials_filtered_by_user_id(self): + """Call ``GET /credentials?user_id={user_id}``.""" + credential = unit.new_credential_ref(user_id=uuid.uuid4().hex) + self.credential_api.create_credential( + credential['id'], credential + ) + + r = self.get('/credentials?user_id=%s' % self.user['id']) + self.assertValidCredentialListResponse(r, ref=self.credential) + for cred in r.result['credentials']: + self.assertEqual(self.user['id'], cred['user_id']) + + def test_create_credential(self): + """Call ``POST /credentials``.""" + ref = unit.new_credential_ref(user_id=self.user['id']) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_get_credential(self): + """Call ``GET /credentials/{credential_id}``.""" + r = self.get( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential['id']}) + self.assertValidCredentialResponse(r, self.credential) + + def test_update_credential(self): + """Call ``PATCH /credentials/{credential_id}``.""" + ref = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + del ref['id'] + r = self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential['id']}, + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_delete_credential(self): + """Call ``DELETE /credentials/{credential_id}``.""" + self.delete( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential['id']}) + + def test_update_credential_non_owner(self): + """Call ``PATCH /credentials/{credential_id}``.""" + alt_user = unit.create_user( + self.identity_api, domain_id=self.domain_id) + alt_user_id = alt_user['id'] + alt_project = unit.new_project_ref(domain_id=self.domain_id) + alt_project_id = alt_project['id'] + self.resource_api.create_project( + alt_project['id'], alt_project) + alt_role = unit.new_role_ref(name='reader') + alt_role_id = alt_role['id'] + self.role_api.create_role(alt_role_id, alt_role) + self.assignment_api.add_role_to_user_and_project( + alt_user_id, alt_project_id, alt_role_id) + auth = self.build_authentication_request( + user_id=alt_user_id, + password=alt_user['password'], + project_id=alt_project_id) + ref = unit.new_credential_ref(user_id=alt_user_id, + project_id=alt_project_id) + r = self.post( + '/credentials', + auth=auth, + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + credential_id = r.result.get('credential')['id'] + + # Cannot change the credential to be owned by another user + update_ref = {'user_id': self.user_id, 'project_id': self.project_id} + self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}, + expected_status=403, + auth=auth, + body={'credential': update_ref}) + + +class TestCredentialTrustScoped(CredentialBaseTestCase): """Test credential with trust scoped token.""" def setUp(self): @@ -392,7 +630,7 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): token_id = r.headers.get('X-Subject-Token') # Create the credential with the trust scoped token - blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + blob, ref = unit.new_ec2_credential(user_id=self.user_id, project_id=self.project_id) r = self.post('/credentials', body={'credential': ref}, token=token_id) @@ -409,6 +647,21 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): self.assertEqual(hashlib.sha256(access).hexdigest(), r.result['credential']['id']) + # Create a role assignment to ensure that it is ignored and only the + # trust-delegated roles are used + role = unit.new_role_ref(name='reader') + role_id = role['id'] + self.role_api.create_role(role_id, role) + self.assignment_api.add_role_to_user_and_project( + self.user_id, self.project_id, role_id) + + ret_blob = json.loads(r.result['credential']['blob']) + ec2token = self._test_get_token( + access=ret_blob['access'], secret=ret_blob['secret']) + ec2_roles = [rl['id'] for rl in ec2token['roles']] + self.assertIn(self.role_id, ec2_roles) + self.assertNotIn(role_id, ec2_roles) + # Create second ec2 credential with the same access key id and check # for conflict. self.post( @@ -418,35 +671,156 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): expected_status=http_client.CONFLICT) +class TestCredentialAccessToken(CredentialBaseTestCase): + """Test credential with access token.""" + + def setUp(self): + super(TestCredentialAccessToken, self).setUp() + self.useFixture( + ksfixtures.KeyRepository( + self.config_fixture, + 'credential', + credential_fernet.MAX_ACTIVE_KEYS + ) + ) + self.base_url = 'http://localhost/v3' + + def _urllib_parse_qs_text_keys(self, content): + results = urllib.parse.parse_qs(content) + return {key.decode('utf-8'): value for key, value in results.items()} + + def _create_single_consumer(self): + endpoint = '/OS-OAUTH1/consumers' + + ref = {'description': uuid.uuid4().hex} + resp = self.post(endpoint, body={'consumer': ref}) + return resp.result['consumer'] + + def _create_request_token(self, consumer, project_id, base_url=None): + endpoint = '/OS-OAUTH1/request_token' + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + signature_method=oauth1.SIG_HMAC, + callback_uri="oob") + headers = {'requested_project_id': project_id} + if not base_url: + base_url = self.base_url + url, headers, body = client.sign(base_url + endpoint, + http_method='POST', + headers=headers) + return endpoint, headers + + def _create_access_token(self, consumer, token, base_url=None): + endpoint = '/OS-OAUTH1/access_token' + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + resource_owner_key=token.key, + resource_owner_secret=token.secret, + signature_method=oauth1.SIG_HMAC, + verifier=token.verifier) + if not base_url: + base_url = self.base_url + url, headers, body = client.sign(base_url + endpoint, + http_method='POST') + headers.update({'Content-Type': 'application/json'}) + return endpoint, headers + + def _get_oauth_token(self, consumer, token): + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + resource_owner_key=token.key, + resource_owner_secret=token.secret, + signature_method=oauth1.SIG_HMAC) + endpoint = '/auth/tokens' + url, headers, body = client.sign(self.base_url + endpoint, + http_method='POST') + headers.update({'Content-Type': 'application/json'}) + ref = {'auth': {'identity': {'oauth1': {}, 'methods': ['oauth1']}}} + return endpoint, headers, ref + + def _authorize_request_token(self, request_id): + if isinstance(request_id, bytes): + request_id = request_id.decode() + return '/OS-OAUTH1/authorize/%s' % (request_id) + + def _get_access_token(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + + url, headers = self._create_request_token(consumer, self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-form-urlencoded') + credentials = self._urllib_parse_qs_text_keys(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + request_token = oauth1.Token(request_key, request_secret) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + resp = self.put(url, body=body, expected_status=http_client.OK) + verifier = resp.result['token']['oauth_verifier'] + + request_token.set_verifier(verifier) + url, headers = self._create_access_token(consumer, request_token) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-form-urlencoded') + credentials = self._urllib_parse_qs_text_keys(content.result) + access_key = credentials['oauth_token'][0] + access_secret = credentials['oauth_token_secret'][0] + access_token = oauth1.Token(access_key, access_secret) + + url, headers, body = self._get_oauth_token(consumer, access_token) + content = self.post(url, headers=headers, body=body) + return access_key, content.headers['X-Subject-Token'] + + def test_access_token_ec2_credential(self): + """Test creating ec2 credential from an oauth access token. + + Call ``POST /credentials``. + """ + access_key, token_id = self._get_access_token() + + # Create the credential with the access token + blob, ref = unit.new_ec2_credential(user_id=self.user_id, + project_id=self.project_id) + r = self.post('/credentials', body={'credential': ref}, token=token_id) + + # We expect the response blob to contain the access_token_id + ret_ref = ref.copy() + ret_blob = blob.copy() + ret_blob['access_token_id'] = access_key.decode('utf-8') + ret_ref['blob'] = json.dumps(ret_blob) + self.assertValidCredentialResponse(r, ref=ret_ref) + + # Assert credential id is same as hash of access key id for + # ec2 credentials + access = blob['access'].encode('utf-8') + self.assertEqual(hashlib.sha256(access).hexdigest(), + r.result['credential']['id']) + + # Create a role assignment to ensure that it is ignored and only the + # roles in the access token are used + role = unit.new_role_ref(name='reader') + role_id = role['id'] + self.role_api.create_role(role_id, role) + self.assignment_api.add_role_to_user_and_project( + self.user_id, self.project_id, role_id) + + ret_blob = json.loads(r.result['credential']['blob']) + ec2token = self._test_get_token( + access=ret_blob['access'], secret=ret_blob['secret']) + ec2_roles = [rl['id'] for rl in ec2token['roles']] + self.assertIn(self.role_id, ec2_roles) + self.assertNotIn(role_id, ec2_roles) + + class TestCredentialEc2(CredentialBaseTestCase): """Test v3 credential compatibility with ec2tokens.""" - def _validate_signature(self, access, secret): - """Test signature validation with the access/secret provided.""" - signer = ec2_utils.Ec2Signer(secret) - params = {'SignatureMethod': 'HmacSHA256', - 'SignatureVersion': '2', - 'AWSAccessKeyId': access} - request = {'host': 'foo', - 'verb': 'GET', - 'path': '/bar', - 'params': params} - signature = signer.generate(request) - - # Now make a request to validate the signed dummy request via the - # ec2tokens API. This proves the v3 ec2 credentials actually work. - sig_ref = {'access': access, - 'signature': signature, - 'host': 'foo', - 'verb': 'GET', - 'path': '/bar', - 'params': params} - r = self.post( - '/ec2tokens', - body={'ec2Credentials': sig_ref}, - expected_status=http_client.OK) - self.assertValidTokenResponse(r) - def test_ec2_credential_signature_validate(self): """Test signature validation with a v3 ec2 credential.""" blob, ref = unit.new_ec2_credential(user_id=self.user['id'], @@ -460,15 +834,15 @@ class TestCredentialEc2(CredentialBaseTestCase): cred_blob = json.loads(r.result['credential']['blob']) self.assertEqual(blob, cred_blob) - self._validate_signature(access=cred_blob['access'], - secret=cred_blob['secret']) + self._test_get_token(access=cred_blob['access'], + secret=cred_blob['secret']) def test_ec2_credential_signature_validate_legacy(self): """Test signature validation with a legacy v3 ec2 credential.""" cred_json, _ = self._create_dict_blob_credential() cred_blob = json.loads(cred_json) - self._validate_signature(access=cred_blob['access'], - secret=cred_blob['secret']) + self._test_get_token(access=cred_blob['access'], + secret=cred_blob['secret']) def _get_ec2_cred_uri(self): return '/users/%s/credentials/OS-EC2' % self.user_id @@ -484,8 +858,8 @@ class TestCredentialEc2(CredentialBaseTestCase): self.assertEqual(self.user_id, ec2_cred['user_id']) self.assertEqual(self.project_id, ec2_cred['tenant_id']) self.assertIsNone(ec2_cred['trust_id']) - self._validate_signature(access=ec2_cred['access'], - secret=ec2_cred['secret']) + self._test_get_token(access=ec2_cred['access'], + secret=ec2_cred['secret']) uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) self.assertThat(ec2_cred['links']['self'], matchers.EndsWith(uri)) diff --git a/keystone/token/providers/common.py b/keystone/token/providers/common.py index 7b4e1b1f67..ed452e6351 100644 --- a/keystone/token/providers/common.py +++ b/keystone/token/providers/common.py @@ -477,7 +477,8 @@ class BaseProvider(base.Provider): auth_context, project_id, domain_id) access_token = None - if 'oauth1' in method_names: + if 'oauth1' in method_names or ( + auth_context and auth_context.get('access_token_id')): access_token_id = auth_context['access_token_id'] access_token = self.oauth_api.get_access_token(access_token_id) diff --git a/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml b/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml new file mode 100644 index 0000000000..656822c2a8 --- /dev/null +++ b/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml @@ -0,0 +1,16 @@ +--- +critical: + - | + [`bug 1872733 `_] + Fixed a critical security issue in which an authenticated user could + escalate their privileges by altering a valid EC2 credential. +security: + - | + [`bug 1872733 `_] + Fixed a critical security issue in which an authenticated user could + escalate their privileges by altering a valid EC2 credential. +fixes: + - | + [`bug 1872733 `_] + Fixed a critical security issue in which an authenticated user could + escalate their privileges by altering a valid EC2 credential. diff --git a/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml b/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml new file mode 100644 index 0000000000..1aed863010 --- /dev/null +++ b/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml @@ -0,0 +1,31 @@ +--- +critical: + - | + [`bug 1872735 `_] + Fixed a security issue in which a trustee or an application credential user + could create an EC2 credential or an application credential that would + permit them to get a token that elevated their role assignments beyond the + subset delegated to them in the trust or application credential. A new + attribute ``app_cred_id`` is now automatically added to the access blob of + an EC2 credential and the role list in the trust or application credential + is respected. +security: + - | + [`bug 1872735 `_] + Fixed a security issue in which a trustee or an application credential user + could create an EC2 credential or an application credential that would + permit them to get a token that elevated their role assignments beyond the + subset delegated to them in the trust or application credential. A new + attribute ``app_cred_id`` is now automatically added to the access blob of + an EC2 credential and the role list in the trust or application credential + is respected. +fixes: + - | + [`bug 1872735 `_] + Fixed a security issue in which a trustee or an application credential user + could create an EC2 credential or an application credential that would + permit them to get a token that elevated their role assignments beyond the + subset delegated to them in the trust or application credential. A new + attribute ``app_cred_id`` is now automatically added to the access blob of + an EC2 credential and the role list in the trust or application credential + is respected. diff --git a/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml b/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml new file mode 100644 index 0000000000..a30259ffa1 --- /dev/null +++ b/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml @@ -0,0 +1,19 @@ +--- +security: + - | + [`bug 1872755 `_] + Added validation to the EC2 credentials update API to ensure the metadata + labels 'trust_id' and 'app_cred_id' are not altered by the user. These + labels are used by keystone to determine the scope allowed by the + credential, and altering these automatic labels could enable an EC2 + credential holder to elevate their access beyond what is permitted by the + application credential or trust that was used to create the EC2 credential. +fixes: + - | + [`bug 1872755 `_] + Added validation to the EC2 credentials update API to ensure the metadata + labels 'trust_id' and 'app_cred_id' are not altered by the user. These + labels are used by keystone to determine the scope allowed by the + credential, and altering these automatic labels could enable an EC2 + credential holder to elevate their access beyond what is permitted by the + application credential or trust that was used to create the EC2 credential.