Fernet payloads for federated scoped tokens.
Federated project and domain scoped tokens need to be able to keep ``OS-FEDERATION`` info within their payloads. Change-Id: I00bbff8066e3842e5a1db4f63ceb7dd9aec083a3 Closes-Bug: #1471289
This commit is contained in:
parent
02f8576ddf
commit
c5b194300a
|
@ -334,6 +334,58 @@ class TestPayloads(tests.TestCase):
|
|||
self.assertEqual(exp_federated_info['protocol_id'],
|
||||
federated_info['protocol_id'])
|
||||
|
||||
def test_federated_project_scoped_payload(self):
|
||||
exp_user_id = 'someNonUuidUserId'
|
||||
exp_methods = ['token']
|
||||
exp_project_id = uuid.uuid4().hex
|
||||
exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True)
|
||||
exp_audit_ids = [provider.random_urlsafe_str()]
|
||||
exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}],
|
||||
'idp_id': uuid.uuid4().hex,
|
||||
'protocol_id': uuid.uuid4().hex}
|
||||
|
||||
payload = token_formatters.FederatedProjectScopedPayload.assemble(
|
||||
exp_user_id, exp_methods, exp_project_id, exp_expires_at,
|
||||
exp_audit_ids, exp_federated_info)
|
||||
|
||||
(user_id, methods, project_id, expires_at, audit_ids,
|
||||
federated_info) = (
|
||||
token_formatters.FederatedProjectScopedPayload.disassemble(
|
||||
payload))
|
||||
|
||||
self.assertEqual(exp_user_id, user_id)
|
||||
self.assertEqual(exp_methods, methods)
|
||||
self.assertEqual(exp_project_id, project_id)
|
||||
self.assertEqual(exp_expires_at, expires_at)
|
||||
self.assertEqual(exp_audit_ids, audit_ids)
|
||||
self.assertDictEqual(exp_federated_info, federated_info)
|
||||
|
||||
def test_federated_domain_scoped_payload(self):
|
||||
exp_user_id = 'someNonUuidUserId'
|
||||
exp_methods = ['token']
|
||||
exp_domain_id = uuid.uuid4().hex
|
||||
exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True)
|
||||
exp_audit_ids = [provider.random_urlsafe_str()]
|
||||
exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}],
|
||||
'idp_id': uuid.uuid4().hex,
|
||||
'protocol_id': uuid.uuid4().hex}
|
||||
|
||||
payload = token_formatters.FederatedDomainScopedPayload.assemble(
|
||||
exp_user_id, exp_methods, exp_domain_id, exp_expires_at,
|
||||
exp_audit_ids, exp_federated_info)
|
||||
|
||||
(user_id, methods, domain_id, expires_at, audit_ids,
|
||||
federated_info) = (
|
||||
token_formatters.FederatedDomainScopedPayload.disassemble(
|
||||
payload))
|
||||
|
||||
self.assertEqual(exp_user_id, user_id)
|
||||
self.assertEqual(exp_methods, methods)
|
||||
self.assertEqual(exp_domain_id, domain_id)
|
||||
self.assertEqual(exp_expires_at, expires_at)
|
||||
self.assertEqual(exp_audit_ids, audit_ids)
|
||||
self.assertDictEqual(exp_federated_info, federated_info)
|
||||
|
||||
|
||||
class TestFernetKeyRotation(tests.TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -127,20 +127,22 @@ class Provider(common.BaseProvider):
|
|||
which unpacks the values and builds the Fernet token.
|
||||
|
||||
"""
|
||||
group_ids = token_data['token'].get('user', {}).get(
|
||||
federation_constants.FEDERATION, {}).get('groups')
|
||||
idp_id = token_data['token'].get('user', {}).get(
|
||||
federation_constants.FEDERATION, {}).get(
|
||||
'identity_provider', {}).get('id')
|
||||
protocol_id = token_data['token'].get('user', {}).get(
|
||||
federation_constants.FEDERATION, {}).get('protocol', {}).get('id')
|
||||
if not group_ids:
|
||||
group_ids = list()
|
||||
if group_ids:
|
||||
federated_dict = dict(group_ids=group_ids, idp_id=idp_id,
|
||||
protocol_id=protocol_id)
|
||||
return federated_dict
|
||||
return None
|
||||
# If we don't have an identity provider ID and a protocol ID, it's safe
|
||||
# to assume we aren't dealing with a federated token.
|
||||
if not (idp_id and protocol_id):
|
||||
return None
|
||||
|
||||
group_ids = token_data['token'].get('user', {}).get(
|
||||
federation_constants.FEDERATION, {}).get('groups')
|
||||
|
||||
return {'group_ids': group_ids,
|
||||
'idp_id': idp_id,
|
||||
'protocol_id': protocol_id}
|
||||
|
||||
def _rebuild_federated_info(self, federated_dict, user_id):
|
||||
"""Format federated information into the token reference.
|
||||
|
@ -158,12 +160,36 @@ class Provider(common.BaseProvider):
|
|||
federated_info = dict(groups=g_ids,
|
||||
identity_provider=dict(id=idp_id),
|
||||
protocol=dict(id=protocol_id))
|
||||
token_dict = {'user': {
|
||||
federation_constants.FEDERATION: federated_info}}
|
||||
token_dict['user']['id'] = user_id
|
||||
token_dict['user']['name'] = user_id
|
||||
token_dict = {
|
||||
'user': {
|
||||
federation_constants.FEDERATION: federated_info,
|
||||
'id': user_id,
|
||||
'name': user_id
|
||||
}
|
||||
}
|
||||
|
||||
return token_dict
|
||||
|
||||
def _rebuild_federated_token_roles(self, token_dict, federated_dict,
|
||||
user_id, project_id, domain_id):
|
||||
"""Populate roles based on (groups, project/domain) pair.
|
||||
|
||||
We must populate roles from (groups, project/domain) as ephemeral users
|
||||
don't exist in the backend. Upon success, a ``roles`` key will be added
|
||||
to ``token_dict``.
|
||||
|
||||
:param token_dict: dictionary with data used for building token
|
||||
:param federated_dict: federated information such as identity provider
|
||||
protocol and set of group IDs
|
||||
:param user_id: user ID
|
||||
:param project_id: project ID the token is being scoped to
|
||||
:param domain_id: domain ID the token is being scoped to
|
||||
|
||||
"""
|
||||
group_ids = [x['id'] for x in federated_dict['group_ids']]
|
||||
self.v3_token_data_helper.populate_roles_for_groups(
|
||||
token_dict, group_ids, project_id, domain_id, user_id)
|
||||
|
||||
def validate_v2_token(self, token_ref):
|
||||
"""Validate a V2 formatted token.
|
||||
|
||||
|
@ -220,6 +246,10 @@ class Provider(common.BaseProvider):
|
|||
trust_ref = None
|
||||
if federated_info:
|
||||
token_dict = self._rebuild_federated_info(federated_info, user_id)
|
||||
if project_id or domain_id:
|
||||
self._rebuild_federated_token_roles(token_dict, federated_info,
|
||||
user_id, project_id,
|
||||
domain_id)
|
||||
if trust_id:
|
||||
trust_ref = self.trust_api.get_trust(trust_id)
|
||||
|
||||
|
|
|
@ -117,6 +117,24 @@ class TokenFormatter(object):
|
|||
expires_at,
|
||||
audit_ids,
|
||||
trust_id)
|
||||
elif project_id and federated_info:
|
||||
version = FederatedProjectScopedPayload.version
|
||||
payload = FederatedProjectScopedPayload.assemble(
|
||||
user_id,
|
||||
methods,
|
||||
project_id,
|
||||
expires_at,
|
||||
audit_ids,
|
||||
federated_info)
|
||||
elif domain_id and federated_info:
|
||||
version = FederatedDomainScopedPayload.version
|
||||
payload = FederatedDomainScopedPayload.assemble(
|
||||
user_id,
|
||||
methods,
|
||||
domain_id,
|
||||
expires_at,
|
||||
audit_ids,
|
||||
federated_info)
|
||||
elif federated_info:
|
||||
version = FederatedPayload.version
|
||||
payload = FederatedPayload.assemble(
|
||||
|
@ -197,6 +215,14 @@ class TokenFormatter(object):
|
|||
elif version == FederatedPayload.version:
|
||||
(user_id, methods, expires_at, audit_ids, federated_info) = (
|
||||
FederatedPayload.disassemble(payload))
|
||||
elif version == FederatedProjectScopedPayload.version:
|
||||
(user_id, methods, project_id, expires_at, audit_ids,
|
||||
federated_info) = FederatedProjectScopedPayload.disassemble(
|
||||
payload)
|
||||
elif version == FederatedDomainScopedPayload.version:
|
||||
(user_id, methods, domain_id, expires_at, audit_ids,
|
||||
federated_info) = FederatedDomainScopedPayload.disassemble(
|
||||
payload)
|
||||
else:
|
||||
# If the token_format is not recognized, raise ValidationError.
|
||||
raise exception.ValidationError(_(
|
||||
|
@ -502,6 +528,15 @@ class TrustScopedPayload(BasePayload):
|
|||
class FederatedPayload(BasePayload):
|
||||
version = 4
|
||||
|
||||
@classmethod
|
||||
def pack_group_id(cls, group_dict):
|
||||
return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id'])
|
||||
|
||||
@classmethod
|
||||
def unpack_group_id(cls, group_id_in_bytes):
|
||||
group_id = cls.attempt_convert_uuid_bytes_to_hex(group_id_in_bytes)
|
||||
return {'id': group_id}
|
||||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, expires_at, audit_ids, federated_info):
|
||||
"""Assemble the payload of a federated token.
|
||||
|
@ -516,12 +551,11 @@ class FederatedPayload(BasePayload):
|
|||
:returns: the payload of a federated token
|
||||
|
||||
"""
|
||||
def pack_group_ids(group_dict):
|
||||
return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id'])
|
||||
|
||||
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
|
||||
methods = auth_plugins.convert_method_list_to_integer(methods)
|
||||
b_group_ids = list(map(pack_group_ids, federated_info['group_ids']))
|
||||
b_group_ids = list(map(cls.pack_group_id,
|
||||
federated_info['group_ids']))
|
||||
b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
|
||||
federated_info['idp_id'])
|
||||
protocol_id = federated_info['protocol_id']
|
||||
|
@ -543,13 +577,10 @@ class FederatedPayload(BasePayload):
|
|||
federated domain ID
|
||||
|
||||
"""
|
||||
def unpack_group_ids(group_id_in_bytes):
|
||||
group_id = cls.attempt_convert_uuid_bytes_to_hex(group_id_in_bytes)
|
||||
return {'id': group_id}
|
||||
|
||||
user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0])
|
||||
methods = auth_plugins.convert_integer_to_method_list(payload[1])
|
||||
group_ids = list(map(unpack_group_ids, payload[2]))
|
||||
group_ids = list(map(cls.unpack_group_id, payload[2]))
|
||||
idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[3])
|
||||
protocol_id = payload[4]
|
||||
expires_at_str = cls._convert_int_to_time_string(payload[5])
|
||||
|
@ -557,3 +588,70 @@ class FederatedPayload(BasePayload):
|
|||
federated_info = dict(group_ids=group_ids, idp_id=idp_id,
|
||||
protocol_id=protocol_id)
|
||||
return (user_id, methods, expires_at_str, audit_ids, federated_info)
|
||||
|
||||
|
||||
class FederatedScopedPayload(FederatedPayload):
|
||||
version = None
|
||||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, scope_id, expires_at, audit_ids,
|
||||
federated_info):
|
||||
"""Assemble the project-scoped payload of a federated token.
|
||||
|
||||
:param user_id: ID of the user in the token request
|
||||
:param methods: list of authentication methods used
|
||||
:param scope_id: ID of the project or domain ID to scope to
|
||||
:param expires_at: datetime of the token's expiration
|
||||
:param audit_ids: list of the token's audit IDs
|
||||
:param federated_info: dictionary containing the identity provider ID,
|
||||
protocol ID, federated domain ID and group IDs
|
||||
:returns: the payload of a federated token
|
||||
|
||||
"""
|
||||
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
|
||||
methods = auth_plugins.convert_method_list_to_integer(methods)
|
||||
b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(scope_id)
|
||||
b_group_ids = list(map(cls.pack_group_id,
|
||||
federated_info['group_ids']))
|
||||
b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
|
||||
federated_info['idp_id'])
|
||||
protocol_id = federated_info['protocol_id']
|
||||
expires_at_int = cls._convert_time_string_to_int(expires_at)
|
||||
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
|
||||
audit_ids))
|
||||
|
||||
return (b_user_id, methods, b_scope_id, b_group_ids, b_idp_id,
|
||||
protocol_id, expires_at_int, b_audit_ids)
|
||||
|
||||
@classmethod
|
||||
def disassemble(cls, payload):
|
||||
"""Validate a project-scoped federated payload.
|
||||
|
||||
:param token_string: a string representing the token
|
||||
:returns: a tuple containing the user_id, auth methods, scope_id,
|
||||
expiration time (as str), audit_ids, and a dictionary
|
||||
containing federated information such as the the identity
|
||||
provider ID, the protocol ID, the federated domain ID and
|
||||
group IDs
|
||||
|
||||
"""
|
||||
user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0])
|
||||
methods = auth_plugins.convert_integer_to_method_list(payload[1])
|
||||
scope_id = cls.attempt_convert_uuid_bytes_to_hex(payload[2])
|
||||
group_ids = list(map(cls.unpack_group_id, payload[3]))
|
||||
idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[4])
|
||||
protocol_id = payload[5]
|
||||
expires_at_str = cls._convert_int_to_time_string(payload[6])
|
||||
audit_ids = list(map(provider.base64_encode, payload[7]))
|
||||
federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
|
||||
group_ids=group_ids)
|
||||
return (user_id, methods, scope_id, expires_at_str, audit_ids,
|
||||
federated_info)
|
||||
|
||||
|
||||
class FederatedProjectScopedPayload(FederatedScopedPayload):
|
||||
version = 5
|
||||
|
||||
|
||||
class FederatedDomainScopedPayload(FederatedScopedPayload):
|
||||
version = 6
|
||||
|
|
Loading…
Reference in New Issue