Make fernet work with oauth1 authentication
Previously, fernet didn't know how to handle oauth1 authentication flows. This patch adds a new token version to the fernet provider and allows it to issue oauth1 tokens. This work is being done so that we can get fernet to be feature equivalent with the uuid token provider. Then we will be slightly closer to making fernet the default token provider in keystone. Closes-Bug: 1534252 Change-Id: I638404952597bb23dff01f80efb728b653e5560c
This commit is contained in:
parent
5c51dbbc0a
commit
03b4e82188
|
@ -30,6 +30,7 @@ from keystone.oauth1 import controllers
|
|||
from keystone.oauth1 import core
|
||||
from keystone.tests import unit
|
||||
from keystone.tests.unit.common import test_notifications
|
||||
from keystone.tests.unit import ksfixtures
|
||||
from keystone.tests.unit.ksfixtures import temporaryfile
|
||||
from keystone.tests.unit import test_v3
|
||||
|
||||
|
@ -599,6 +600,18 @@ class AuthTokenTests(OAuthFlowTests):
|
|||
expected_status=http_client.FORBIDDEN)
|
||||
|
||||
|
||||
class FernetAuthTokenTests(AuthTokenTests):
|
||||
|
||||
def config_overrides(self):
|
||||
super(FernetAuthTokenTests, self).config_overrides()
|
||||
self.config_fixture.config(group='token', provider='fernet')
|
||||
self.useFixture(ksfixtures.KeyRepository(self.config_fixture))
|
||||
|
||||
def test_delete_keystone_tokens_by_consumer_id(self):
|
||||
# NOTE(lbragstad): Fernet tokens are never persisted in the backend.
|
||||
pass
|
||||
|
||||
|
||||
class MaliciousOAuth1Tests(OAuth1Tests):
|
||||
|
||||
def test_bad_consumer_secret(self):
|
||||
|
|
|
@ -338,7 +338,8 @@ class TestPayloads(unit.TestCase):
|
|||
|
||||
def _test_payload(self, payload_class, exp_user_id=None, exp_methods=None,
|
||||
exp_project_id=None, exp_domain_id=None,
|
||||
exp_trust_id=None, exp_federated_info=None):
|
||||
exp_trust_id=None, exp_federated_info=None,
|
||||
exp_access_token_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,10 +347,13 @@ class TestPayloads(unit.TestCase):
|
|||
|
||||
payload = payload_class.assemble(
|
||||
exp_user_id, exp_methods, exp_project_id, exp_domain_id,
|
||||
exp_expires_at, exp_audit_ids, exp_trust_id, exp_federated_info)
|
||||
exp_expires_at, exp_audit_ids, exp_trust_id, exp_federated_info,
|
||||
exp_access_token_id)
|
||||
|
||||
(user_id, methods, project_id, domain_id, expires_at, audit_ids,
|
||||
trust_id, federated_info) = payload_class.disassemble(payload)
|
||||
(user_id, methods, project_id,
|
||||
domain_id, expires_at, audit_ids,
|
||||
trust_id, federated_info,
|
||||
access_token_id) = payload_class.disassemble(payload)
|
||||
|
||||
self.assertEqual(exp_user_id, user_id)
|
||||
self.assertEqual(exp_methods, methods)
|
||||
|
@ -358,6 +362,7 @@ class TestPayloads(unit.TestCase):
|
|||
self.assertEqual(exp_project_id, project_id)
|
||||
self.assertEqual(exp_domain_id, domain_id)
|
||||
self.assertEqual(exp_trust_id, trust_id)
|
||||
self.assertEqual(exp_access_token_id, access_token_id)
|
||||
|
||||
if exp_federated_info:
|
||||
self.assertDictEqual(exp_federated_info, federated_info)
|
||||
|
@ -463,6 +468,11 @@ class TestPayloads(unit.TestCase):
|
|||
exp_domain_id=uuid.uuid4().hex,
|
||||
exp_federated_info=exp_federated_info)
|
||||
|
||||
def test_oauth_scoped_payload(self):
|
||||
self._test_payload(token_formatters.OauthScopedPayload,
|
||||
exp_project_id=uuid.uuid4().hex,
|
||||
exp_access_token_id=uuid.uuid4().hex)
|
||||
|
||||
|
||||
class TestFernetKeyRotation(unit.TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -84,6 +84,11 @@ class V2TokenDataHelper(object):
|
|||
'API.')
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
if 'OS-OAUTH1' in v3_token:
|
||||
msg = ('Unable to validate Oauth tokens using the version v2.0 '
|
||||
'API.')
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
# Set user roles
|
||||
user['roles'] = []
|
||||
role_ids = []
|
||||
|
@ -702,7 +707,7 @@ class BaseProvider(provider.Provider):
|
|||
def validate_non_persistent_token(self, token_id):
|
||||
try:
|
||||
(user_id, methods, audit_ids, domain_id, project_id, trust_id,
|
||||
federated_info, created_at, expires_at) = (
|
||||
federated_info, access_token_id, created_at, expires_at) = (
|
||||
self.token_formatter.validate_token(token_id))
|
||||
except exception.ValidationError as e:
|
||||
raise exception.TokenNotFound(e)
|
||||
|
@ -725,6 +730,10 @@ class BaseProvider(provider.Provider):
|
|||
if trust_id:
|
||||
trust_ref = self.trust_api.get_trust(trust_id)
|
||||
|
||||
access_token = None
|
||||
if access_token_id:
|
||||
access_token = self.oauth_api.get_access_token(access_token_id)
|
||||
|
||||
return self.v3_token_data_helper.get_token_data(
|
||||
user_id,
|
||||
method_names=methods,
|
||||
|
@ -734,6 +743,7 @@ class BaseProvider(provider.Provider):
|
|||
expires=expires_at,
|
||||
trust=trust_ref,
|
||||
token=token_dict,
|
||||
access_token=access_token,
|
||||
audit_info=audit_ids)
|
||||
|
||||
def validate_v3_token(self, token_ref):
|
||||
|
|
|
@ -23,7 +23,7 @@ from keystone.token.providers.fernet import token_formatters as tf
|
|||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@dependency.requires('trust_api')
|
||||
@dependency.requires('trust_api', 'oauth_api')
|
||||
class Provider(common.BaseProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Provider, self).__init__(*args, **kwargs)
|
||||
|
@ -154,9 +154,10 @@ class Provider(common.BaseProvider):
|
|||
project_id = token_data['access']['token'].get('tenant', {}).get('id')
|
||||
domain_id = None
|
||||
trust_id = None
|
||||
access_token_id = None
|
||||
federated_info = None
|
||||
return (user_id, expires_at, audit_ids, methods, domain_id, project_id,
|
||||
trust_id, federated_info)
|
||||
trust_id, access_token_id, federated_info)
|
||||
|
||||
def _extract_v3_token_data(self, token_data):
|
||||
"""Extract information from a v3 token reference."""
|
||||
|
@ -167,10 +168,12 @@ class Provider(common.BaseProvider):
|
|||
domain_id = token_data['token'].get('domain', {}).get('id')
|
||||
project_id = token_data['token'].get('project', {}).get('id')
|
||||
trust_id = token_data['token'].get('OS-TRUST:trust', {}).get('id')
|
||||
access_token_id = token_data['token'].get('OS-OAUTH1', {}).get(
|
||||
'access_token_id')
|
||||
federated_info = self._build_federated_info(token_data)
|
||||
|
||||
return (user_id, expires_at, audit_ids, methods, domain_id, project_id,
|
||||
trust_id, federated_info)
|
||||
trust_id, access_token_id, federated_info)
|
||||
|
||||
def _get_token_id(self, token_data):
|
||||
"""Generate the token_id based upon the data in token_data.
|
||||
|
@ -184,21 +187,24 @@ class Provider(common.BaseProvider):
|
|||
# attribute.
|
||||
if token_data.get('access'):
|
||||
(user_id, expires_at, audit_ids, methods, domain_id, project_id,
|
||||
trust_id, federated_info) = self._extract_v2_token_data(
|
||||
token_data)
|
||||
trust_id, access_token_id, federated_info) = (
|
||||
self._extract_v2_token_data(token_data))
|
||||
else:
|
||||
(user_id, expires_at, audit_ids, methods, domain_id, project_id,
|
||||
trust_id, federated_info) = self._extract_v3_token_data(
|
||||
token_data)
|
||||
trust_id, access_token_id, federated_info) = (
|
||||
self._extract_v3_token_data(token_data))
|
||||
|
||||
return self.token_formatter.create_token(user_id,
|
||||
expires_at,
|
||||
audit_ids,
|
||||
methods=methods,
|
||||
domain_id=domain_id,
|
||||
project_id=project_id,
|
||||
trust_id=trust_id,
|
||||
federated_info=federated_info)
|
||||
return self.token_formatter.create_token(
|
||||
user_id,
|
||||
expires_at,
|
||||
audit_ids,
|
||||
methods=methods,
|
||||
domain_id=domain_id,
|
||||
project_id=project_id,
|
||||
trust_id=trust_id,
|
||||
federated_info=federated_info,
|
||||
access_token_id=access_token_id
|
||||
)
|
||||
|
||||
@property
|
||||
def _supports_bind_authentication(self):
|
||||
|
|
|
@ -145,18 +145,19 @@ class TokenFormatter(object):
|
|||
|
||||
def create_token(self, user_id, expires_at, audit_ids, methods=None,
|
||||
domain_id=None, project_id=None, trust_id=None,
|
||||
federated_info=None):
|
||||
federated_info=None, access_token_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,
|
||||
trust_id=trust_id, federated_info=federated_info):
|
||||
trust_id=trust_id, federated_info=federated_info,
|
||||
access_token_id=access_token_id):
|
||||
break
|
||||
|
||||
version = payload_class.version
|
||||
payload = payload_class.assemble(
|
||||
user_id, methods, project_id, domain_id, expires_at, audit_ids,
|
||||
trust_id, federated_info
|
||||
trust_id, federated_info, access_token_id
|
||||
)
|
||||
|
||||
versioned_payload = (version,) + payload
|
||||
|
@ -189,7 +190,7 @@ class TokenFormatter(object):
|
|||
for payload_class in PAYLOAD_CLASSES:
|
||||
if version == payload_class.version:
|
||||
(user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info) = (
|
||||
audit_ids, trust_id, federated_info, access_token_id) = (
|
||||
payload_class.disassemble(payload))
|
||||
break
|
||||
else:
|
||||
|
@ -206,7 +207,7 @@ class TokenFormatter(object):
|
|||
expires_at = ks_utils.isotime(at=expires_at, subsecond=True)
|
||||
|
||||
return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
|
||||
federated_info, created_at, expires_at)
|
||||
federated_info, access_token_id, created_at, expires_at)
|
||||
|
||||
|
||||
class BasePayload(object):
|
||||
|
@ -226,7 +227,7 @@ class BasePayload(object):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_id):
|
||||
"""Assemble the payload of a token.
|
||||
|
||||
:param user_id: identifier of the user in the token request
|
||||
|
@ -239,6 +240,7 @@ class BasePayload(object):
|
|||
:param federated_info: dictionary containing group IDs, the identity
|
||||
provider ID, protocol ID, and federated domain
|
||||
ID
|
||||
:param access_token_id: ID of the secret in OAuth1 authentication
|
||||
:returns: the payload of a token
|
||||
|
||||
"""
|
||||
|
@ -251,7 +253,7 @@ class BasePayload(object):
|
|||
The tuple consists of::
|
||||
|
||||
(user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
* ``methods`` are the auth methods.
|
||||
* federated_info is a dict contains the group IDs, the identity
|
||||
|
@ -336,7 +338,7 @@ class UnscopedPayload(BasePayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_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)
|
||||
|
@ -356,8 +358,9 @@ class UnscopedPayload(BasePayload):
|
|||
domain_id = None
|
||||
trust_id = None
|
||||
federated_info = None
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class DomainScopedPayload(BasePayload):
|
||||
|
@ -369,7 +372,7 @@ class DomainScopedPayload(BasePayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_id):
|
||||
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
|
||||
methods = auth_plugins.convert_method_list_to_integer(methods)
|
||||
try:
|
||||
|
@ -404,9 +407,9 @@ class DomainScopedPayload(BasePayload):
|
|||
project_id = None
|
||||
trust_id = None
|
||||
federated_info = None
|
||||
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class ProjectScopedPayload(BasePayload):
|
||||
|
@ -418,7 +421,7 @@ class ProjectScopedPayload(BasePayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_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)
|
||||
|
@ -441,9 +444,9 @@ class ProjectScopedPayload(BasePayload):
|
|||
domain_id = None
|
||||
trust_id = None
|
||||
federated_info = None
|
||||
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class TrustScopedPayload(BasePayload):
|
||||
|
@ -455,7 +458,7 @@ class TrustScopedPayload(BasePayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_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)
|
||||
|
@ -481,9 +484,9 @@ class TrustScopedPayload(BasePayload):
|
|||
trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
|
||||
domain_id = None
|
||||
federated_info = None
|
||||
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class FederatedUnscopedPayload(BasePayload):
|
||||
|
@ -506,7 +509,7 @@ class FederatedUnscopedPayload(BasePayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_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,
|
||||
|
@ -539,8 +542,9 @@ class FederatedUnscopedPayload(BasePayload):
|
|||
project_id = None
|
||||
domain_id = None
|
||||
trust_id = None
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class FederatedScopedPayload(FederatedUnscopedPayload):
|
||||
|
@ -548,7 +552,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
|
|||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info):
|
||||
audit_ids, trust_id, federated_info, access_token_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(
|
||||
|
@ -590,8 +594,9 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
|
|||
federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
|
||||
group_ids=group_ids)
|
||||
trust_id = None
|
||||
access_token_id = None
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info)
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
class FederatedProjectScopedPayload(FederatedScopedPayload):
|
||||
|
@ -610,6 +615,49 @@ class FederatedDomainScopedPayload(FederatedScopedPayload):
|
|||
return kwargs['domain_id'] and kwargs['federated_info']
|
||||
|
||||
|
||||
class OauthScopedPayload(BasePayload):
|
||||
version = 7
|
||||
|
||||
@classmethod
|
||||
def create_arguments_apply(cls, **kwargs):
|
||||
return kwargs['access_token_id']
|
||||
|
||||
@classmethod
|
||||
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
|
||||
audit_ids, trust_id, federated_info, access_token_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(provider.random_urlsafe_str_to_bytes,
|
||||
audit_ids))
|
||||
b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes(
|
||||
access_token_id)
|
||||
return (b_user_id, methods, b_project_id, b_access_token_id,
|
||||
expires_at_int, b_audit_ids)
|
||||
|
||||
@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)
|
||||
(is_stored_as_bytes, access_token_id) = payload[3]
|
||||
if is_stored_as_bytes:
|
||||
access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id)
|
||||
expires_at_str = cls._convert_float_to_time_string(payload[4])
|
||||
audit_ids = list(map(provider.base64_encode, payload[5]))
|
||||
domain_id = None
|
||||
trust_id = None
|
||||
federated_info = None
|
||||
|
||||
return (user_id, methods, project_id, domain_id, expires_at_str,
|
||||
audit_ids, trust_id, federated_info, access_token_id)
|
||||
|
||||
|
||||
# For now, the order of the classes in the following list is important. This
|
||||
# is because the way they test that the payload applies to them in
|
||||
# the create_arguments_apply method requires that the previous ones rejected
|
||||
|
@ -618,6 +666,7 @@ class FederatedDomainScopedPayload(FederatedScopedPayload):
|
|||
# TODO(blk-u): Clean up the create_arguments_apply methods so that they don't
|
||||
# depend on the previous classes then these can be in any order.
|
||||
PAYLOAD_CLASSES = [
|
||||
OauthScopedPayload,
|
||||
TrustScopedPayload,
|
||||
FederatedProjectScopedPayload,
|
||||
FederatedDomainScopedPayload,
|
||||
|
|
Loading…
Reference in New Issue