Populate application credential data in token

Without this patch, the token formatter does not have enough data to
construct a token created with an application credential. This means
that if the token cache is disabled or expired, when keystone goes to
create the token it will not find any application credential information
and will not recreate the application_credential_restricted parameter in
the token data. This patch creates a new Payload class for application
credentials so that the application credential ID is properly persisted
in the msgpack'd payload. It also adds more data to the token data
object so that the application credential ID and name as well as its
restricted status is available when the token is queried.

Co-authored-by: Lance Bragstad <lbragstad@gmail.com>

Change-Id: I322a40404d8287748fe8c3a8d6dc1256d935d84a
Closes-bug: #1750415
This commit is contained in:
Lance Bragstad 2018-02-19 18:23:25 +00:00 committed by Colleen Murphy
parent db91bfc8c8
commit 796198f196
8 changed files with 144 additions and 35 deletions

View File

@ -86,7 +86,8 @@ class ApplicationCredentialV3(controller.V3Controller):
def _check_unrestricted(self, token):
auth_methods = token['methods']
if 'application_credential' in auth_methods:
if token.token_data['token']['application_credential_restricted']:
td = token.token_data['token']
if td['application_credential']['restricted']:
action = _("Using method 'application_credential' is not "
"allowed for managing additional application "
"credentials.")

View File

@ -5300,6 +5300,27 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
app_cred_id=app_cred_ref['id'], secret=app_cred_ref['secret'])
self.v3_create_token(auth_data, expected_status=http_client.CREATED)
def test_validate_application_credential_token_populates_restricted(self):
self.config_fixture.config(group='token', cache_on_issue=False)
app_cred = self._make_app_cred()
app_cred_ref = self.app_cred_api.create_application_credential(
app_cred)
auth_data = self.build_authentication_request(
app_cred_id=app_cred_ref['id'], secret=app_cred_ref['secret'])
auth_response = self.v3_create_token(
auth_data, expected_status=http_client.CREATED)
self.assertTrue(
auth_response.json['token']['application_credential']['restricted']
)
token_id = auth_response.headers.get('X-Subject-Token')
headers = {'X-Auth-Token': token_id, 'X-Subject-Token': token_id}
validate_response = self.get(
'/auth/tokens', headers=headers
).json_body
self.assertTrue(
validate_response['token']['application_credential']['restricted']
)
def test_valid_application_credential_with_name_succeeds(self):
app_cred = self._make_app_cred()
app_cred_ref = self.app_cred_api.create_application_credential(

View File

@ -337,7 +337,8 @@ class TestPayloads(unit.TestCase):
def _test_payload(self, payload_class, exp_user_id=None, exp_methods=None,
exp_system=None, exp_project_id=None,
exp_domain_id=None, exp_trust_id=None,
exp_federated_info=None, exp_access_token_id=None):
exp_federated_info=None, exp_access_token_id=None,
exp_app_cred_id=None):
exp_user_id = exp_user_id or uuid.uuid4().hex
exp_methods = exp_methods or ['password']
exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True)
@ -346,12 +347,12 @@ class TestPayloads(unit.TestCase):
payload = payload_class.assemble(
exp_user_id, exp_methods, exp_system, exp_project_id,
exp_domain_id, exp_expires_at, exp_audit_ids, exp_trust_id,
exp_federated_info, exp_access_token_id)
exp_federated_info, exp_access_token_id, exp_app_cred_id)
(user_id, methods, system, project_id,
domain_id, expires_at, audit_ids,
trust_id, federated_info,
access_token_id) = payload_class.disassemble(payload)
access_token_id, app_cred_id) = payload_class.disassemble(payload)
self.assertEqual(exp_user_id, user_id)
self.assertEqual(exp_methods, methods)
@ -362,6 +363,7 @@ class TestPayloads(unit.TestCase):
self.assertEqual(exp_domain_id, domain_id)
self.assertEqual(exp_trust_id, trust_id)
self.assertEqual(exp_access_token_id, access_token_id)
self.assertEqual(exp_app_cred_id, app_cred_id)
if exp_federated_info:
self.assertDictEqual(exp_federated_info, federated_info)
@ -476,6 +478,18 @@ class TestPayloads(unit.TestCase):
exp_project_id=uuid.uuid4().hex,
exp_access_token_id=uuid.uuid4().hex)
def test_app_cred_scoped_payload_with_non_uuid_ids(self):
self._test_payload(token_formatters.ApplicationCredentialScopedPayload,
exp_user_id='someNonUuidUserId',
exp_project_id='someNonUuidProjectId',
exp_app_cred_id='someNonUuidAppCredId')
def test_app_cred_scoped_payload_with_16_char_non_uuid_ids(self):
self._test_payload(token_formatters.ApplicationCredentialScopedPayload,
exp_user_id='0123456789abcdef',
exp_project_id='0123456789abcdef',
exp_app_cred_id='0123456789abcdef')
class TestFernetKeyRotation(unit.TestCase):
def setUp(self):

View File

@ -488,12 +488,15 @@ class V3TokenDataHelper(provider_api.ProviderAPIMixin, object):
LOG.error(msg)
raise exception.UnexpectedError(msg)
def _populate_app_cred_restrictions(self, token_data, app_cred_id):
def _populate_app_cred(self, token_data, app_cred_id):
if app_cred_id:
app_cred_api = PROVIDERS.application_credential_api
app_cred = app_cred_api.get_application_credential(app_cred_id)
restricted = not app_cred['unrestricted']
token_data['application_credential_restricted'] = restricted
token_data['application_credential'] = {}
token_data['application_credential']['id'] = app_cred['id']
token_data['application_credential']['name'] = app_cred['name']
token_data['application_credential']['restricted'] = restricted
def get_token_data(self, user_id, method_names, system=None,
domain_id=None, project_id=None, expires=None,
@ -528,7 +531,7 @@ class V3TokenDataHelper(provider_api.ProviderAPIMixin, object):
self._populate_token_dates(token_data, expires=expires,
issued_at=issued_at)
self._populate_oauth_section(token_data, access_token)
self._populate_app_cred_restrictions(token_data, app_cred_id)
self._populate_app_cred(token_data, app_cred_id)
return {'token': token_data}
@ -644,7 +647,7 @@ class BaseProvider(provider_api.ProviderAPIMixin, base.Provider):
try:
(user_id, methods, audit_ids, system, domain_id,
project_id, trust_id, federated_info, access_token_id,
issued_at, expires_at) = (
app_cred_id, issued_at, expires_at) = (
self.token_formatter.validate_token(token_id))
except exception.ValidationError as e:
raise exception.TokenNotFound(e)
@ -693,4 +696,5 @@ class BaseProvider(provider_api.ProviderAPIMixin, base.Provider):
token=token_dict,
bind=bind,
access_token=access_token,
audit_info=audit_ids)
audit_info=audit_ids,
app_cred_id=app_cred_id)

View File

@ -166,6 +166,8 @@ class Provider(common.BaseProvider):
access_token_id = token_data['token'].get('OS-OAUTH1', {}).get(
'access_token_id')
federated_info = self._build_federated_info(token_data)
app_cred_id = token_data['token'].get('application_credential',
{}).get('id')
return self.token_formatter.create_token(
user_id,
@ -177,7 +179,8 @@ class Provider(common.BaseProvider):
project_id=project_id,
trust_id=trust_id,
federated_info=federated_info,
access_token_id=access_token_id
access_token_id=access_token_id,
app_cred_id=app_cred_id
)
@property

View File

@ -137,20 +137,22 @@ class TokenFormatter(object):
def create_token(self, user_id, expires_at, audit_ids, methods=None,
system=None, domain_id=None, project_id=None,
trust_id=None, federated_info=None, access_token_id=None):
trust_id=None, federated_info=None, access_token_id=None,
app_cred_id=None):
"""Given a set of payload attributes, generate a Fernet token."""
for payload_class in PAYLOAD_CLASSES:
if payload_class.create_arguments_apply(
project_id=project_id, domain_id=domain_id,
system=system, trust_id=trust_id,
federated_info=federated_info,
access_token_id=access_token_id):
access_token_id=access_token_id,
app_cred_id=app_cred_id):
break
version = payload_class.version
payload = payload_class.assemble(
user_id, methods, system, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info, access_token_id
audit_ids, trust_id, federated_info, access_token_id, app_cred_id
)
versioned_payload = (version,) + payload
@ -184,7 +186,8 @@ class TokenFormatter(object):
if version == payload_class.version:
(user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id) = payload_class.disassemble(payload)
access_token_id,
app_cred_id) = payload_class.disassemble(payload)
break
else:
# If the token_format is not recognized, raise ValidationError.
@ -200,8 +203,8 @@ class TokenFormatter(object):
expires_at = ks_utils.isotime(at=expires_at, subsecond=True)
return (user_id, methods, audit_ids, system, domain_id, project_id,
trust_id, federated_info, access_token_id, issued_at,
expires_at)
trust_id, federated_info, access_token_id, app_cred_id,
issued_at, expires_at)
class BasePayload(object):
@ -222,7 +225,7 @@ class BasePayload(object):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
"""Assemble the payload of a token.
:param user_id: identifier of the user in the token request
@ -237,6 +240,7 @@ class BasePayload(object):
provider ID, protocol ID, and federated domain
ID
:param access_token_id: ID of the secret in OAuth1 authentication
:param app_cred_id: ID of the application credential in effect
:returns: the payload of a token
"""
@ -250,7 +254,7 @@ class BasePayload(object):
(user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
* ``methods`` are the auth methods.
* federated_info is a dict contains the group IDs, the identity
@ -362,7 +366,7 @@ class UnscopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
expires_at_int = cls._convert_time_string_to_float(expires_at)
@ -384,9 +388,10 @@ class UnscopedPayload(BasePayload):
trust_id = None
federated_info = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class DomainScopedPayload(BasePayload):
@ -399,7 +404,7 @@ class DomainScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
try:
@ -438,9 +443,10 @@ class DomainScopedPayload(BasePayload):
trust_id = None
federated_info = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class ProjectScopedPayload(BasePayload):
@ -453,7 +459,7 @@ class ProjectScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
@ -478,9 +484,10 @@ class ProjectScopedPayload(BasePayload):
trust_id = None
federated_info = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class TrustScopedPayload(BasePayload):
@ -493,7 +500,7 @@ class TrustScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
@ -521,9 +528,10 @@ class TrustScopedPayload(BasePayload):
domain_id = None
federated_info = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class FederatedUnscopedPayload(BasePayload):
@ -547,7 +555,7 @@ class FederatedUnscopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_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(cls.pack_group_id,
@ -586,9 +594,10 @@ class FederatedUnscopedPayload(BasePayload):
domain_id = None
trust_id = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class FederatedScopedPayload(FederatedUnscopedPayload):
@ -597,7 +606,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
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(
@ -641,9 +650,10 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
system = None
trust_id = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class FederatedProjectScopedPayload(FederatedScopedPayload):
@ -672,7 +682,7 @@ class OauthScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
@ -702,10 +712,11 @@ class OauthScopedPayload(BasePayload):
domain_id = None
trust_id = None
federated_info = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class SystemScopedPayload(BasePayload):
@ -718,7 +729,7 @@ class SystemScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id):
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
expires_at_int = cls._convert_time_string_to_float(expires_at)
@ -740,9 +751,55 @@ class SystemScopedPayload(BasePayload):
trust_id = None
federated_info = None
access_token_id = None
app_cred_id = None
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id)
access_token_id, app_cred_id)
class ApplicationCredentialScopedPayload(BasePayload):
version = 9
@classmethod
def create_arguments_apply(cls, **kwargs):
return kwargs['app_cred_id']
@classmethod
def assemble(cls, user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id, app_cred_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
b_app_cred_id = cls.attempt_convert_uuid_hex_to_bytes(app_cred_id)
return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
b_app_cred_id)
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
if is_stored_as_bytes:
user_id = cls.convert_uuid_bytes_to_hex(user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
if is_stored_as_bytes:
project_id = cls.convert_uuid_bytes_to_hex(project_id)
expires_at_str = cls._convert_float_to_time_string(payload[3])
audit_ids = list(map(cls.base64_encode, payload[4]))
system = None
domain_id = None
trust_id = None
federated_info = None
access_token_id = None
(is_stored_as_bytes, app_cred_id) = payload[5]
if is_stored_as_bytes:
app_cred_id = cls.convert_uuid_bytes_to_hex(app_cred_id)
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id, app_cred_id)
# For now, the order of the classes in the following list is important. This
@ -758,6 +815,7 @@ PAYLOAD_CLASSES = [
FederatedProjectScopedPayload,
FederatedDomainScopedPayload,
FederatedUnscopedPayload,
ApplicationCredentialScopedPayload,
ProjectScopedPayload,
DomainScopedPayload,
SystemScopedPayload,

View File

@ -103,7 +103,8 @@ class TrustV3(controller.V3Controller):
def _check_unrestricted(self, token):
auth_methods = token['methods']
if 'application_credential' in auth_methods:
if token.token_data['token']['application_credential_restricted']:
td = token.token_data['token']
if td['application_credential']['restricted']:
action = _("Using method 'application_credential' is not "
"allowed for managing trusts.")
raise exception.ForbiddenAction(action=action)

View File

@ -0,0 +1,7 @@
---
fixes:
- |
[`bug 1750415 <https://bugs.launchpad.net/keystone/+bug/1750415>`_]
Fixes an implementation fault in application credentials where the
application credential reference was not populated in the token data,
causing problems with the token validation when caching was disabled.