summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--keystone/contrib/ec2/controllers.py52
-rw-r--r--keystone/credential/controllers.py70
-rw-r--r--keystone/tests/unit/test_v3_credential.py444
-rw-r--r--keystone/token/providers/common.py3
-rw-r--r--releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml16
-rw-r--r--releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml31
-rw-r--r--releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml19
7 files changed, 568 insertions, 67 deletions
diff --git a/keystone/contrib/ec2/controllers.py b/keystone/contrib/ec2/controllers.py
index 390d45fe5..b54c0886c 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 867694e31..3dcf492d9 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 ebfcbffd6..bf113e7be 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,34 +671,155 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase):
expected_status=http_client.CONFLICT)
-class TestCredentialEc2(CredentialBaseTestCase):
- """Test v3 credential compatibility with ec2tokens."""
+class TestCredentialAccessToken(CredentialBaseTestCase):
+ """Test credential with access token."""
- 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)
+ 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.
- # 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)
+ 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 test_ec2_credential_signature_validate(self):
"""Test signature validation with a v3 ec2 credential."""
@@ -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 7b4e1b1f6..ed452e635 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 000000000..656822c2a
--- /dev/null
+++ b/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml
@@ -0,0 +1,16 @@
+---
+critical:
+ - |
+ [`bug 1872733 <https://bugs.launchpad.net/keystone/+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 <https://bugs.launchpad.net/keystone/+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 <https://bugs.launchpad.net/keystone/+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 000000000..1aed86301
--- /dev/null
+++ b/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml
@@ -0,0 +1,31 @@
+---
+critical:
+ - |
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+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 <https://bugs.launchpad.net/keystone/+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 <https://bugs.launchpad.net/keystone/+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 000000000..a30259ffa
--- /dev/null
+++ b/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml
@@ -0,0 +1,19 @@
+---
+security:
+ - |
+ [`bug 1872755 <https://bugs.launchpad.net/keystone/+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 <https://bugs.launchpad.net/keystone/+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.