Remove backend dependencies from token provider

The token provider module (core) has a list of methods that are called
by backend modules. This patch moves those methods to the backend and
removes dependencies where backend code references code in the core.

Change-Id: I59dab2efc5b743508ee9ecebcdc7c07b9f66791d
Closes-Bug: #1563101
This commit is contained in:
Ronald De Rose 2016-10-13 18:34:55 +00:00
parent dd9145e127
commit 980554a8ab
9 changed files with 134 additions and 130 deletions

View File

@ -24,7 +24,7 @@ from keystone.models import revoke_model
from keystone.revoke.backends import sql
from keystone.tests import unit
from keystone.tests.unit import test_backend_sql
from keystone.token import provider
from keystone.token.providers import common
def _new_id():
@ -245,7 +245,7 @@ class RevokeTests(object):
# check to make sure that list_events matches the token to the event we
# just revoked.
first_token = _sample_blank_token()
first_token['audit_id'] = provider.random_urlsafe_str()
first_token['audit_id'] = common.random_urlsafe_str()
add_event(events, revoke_model.RevokeEvent(
audit_id=first_token['audit_id']))
self.revoke_api.revoke_by_audit_id(
@ -258,7 +258,7 @@ class RevokeTests(object):
# sure that list events only finds 1 match since there are 2 and they
# dont both have different populated audit_id fields
second_token = _sample_blank_token()
second_token['audit_id'] = provider.random_urlsafe_str()
second_token['audit_id'] = common.random_urlsafe_str()
add_event(events, revoke_model.RevokeEvent(
audit_id=second_token['audit_id']))
self.revoke_api.revoke_by_audit_id(
@ -294,7 +294,7 @@ class RevokeTests(object):
first_token = _sample_blank_token()
first_token['user_id'] = uuid.uuid4().hex
first_token['project_id'] = uuid.uuid4().hex
first_token['audit_id'] = provider.random_urlsafe_str()
first_token['audit_id'] = common.random_urlsafe_str()
# revoke event and then verify that that there is only one revocation
# and verify the only revoked event is the token
add_event(events, revoke_model.RevokeEvent(
@ -327,7 +327,7 @@ class RevokeTests(object):
fourth_token = _sample_blank_token()
fourth_token['user_id'] = uuid.uuid4().hex
fourth_token['project_id'] = uuid.uuid4().hex
fourth_token['audit_id'] = provider.random_urlsafe_str()
fourth_token['audit_id'] = common.random_urlsafe_str()
add_event(events, revoke_model.RevokeEvent(
project_id=fourth_token['project_id'],
audit_id=fourth_token['audit_id']))
@ -544,7 +544,7 @@ class RevokeListTests(unit.TestCase):
self._user_field_test('trustor_id')
def test_revoke_by_audit_id(self):
audit_id = provider.audit_info(parent_audit_id=None)[0]
audit_id = common.build_audit_info(parent_audit_id=None)[0]
token_data_1 = _sample_blank_token()
# Audit ID and Audit Chain ID are populated with the same value
# if the token is an original token
@ -553,7 +553,7 @@ class RevokeListTests(unit.TestCase):
event = self._revoke_by_audit_id(audit_id)
self._assertTokenRevoked(token_data_1)
audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0]
audit_id_2 = common.build_audit_info(parent_audit_id=audit_id)[0]
token_data_2 = _sample_blank_token()
token_data_2['audit_id'] = audit_id_2
token_data_2['audit_chain_id'] = audit_id
@ -563,7 +563,7 @@ class RevokeListTests(unit.TestCase):
self._assertTokenNotRevoked(token_data_1)
def test_revoke_by_audit_chain_id(self):
audit_id = provider.audit_info(parent_audit_id=None)[0]
audit_id = common.build_audit_info(parent_audit_id=None)[0]
token_data_1 = _sample_blank_token()
# Audit ID and Audit Chain ID are populated with the same value
# if the token is an original token
@ -572,7 +572,7 @@ class RevokeListTests(unit.TestCase):
event = self._revoke_by_audit_chain_id(audit_id)
self._assertTokenRevoked(token_data_1)
audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0]
audit_id_2 = common.build_audit_info(parent_audit_id=audit_id)[0]
token_data_2 = _sample_blank_token()
token_data_2['audit_id'] = audit_id_2
token_data_2['audit_chain_id'] = audit_id

View File

@ -10,21 +10,16 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from six.moves import urllib
from keystone.tests import unit
from keystone.token import provider
from keystone.token.providers import common
class TestRandomStrings(unit.BaseTestCase):
class TestTokenProvidersCommon(unit.TestCase):
def setUp(self):
super(TestTokenProvidersCommon, self).setUp()
def test_strings_are_url_safe(self):
s = provider.random_urlsafe_str()
s = common.random_urlsafe_str()
self.assertEqual(s, urllib.parse.quote_plus(s))
def test_strings_can_be_converted_to_bytes(self):
s = provider.random_urlsafe_str()
self.assertIsInstance(s, six.text_type)
b = provider.random_urlsafe_str_to_bytes(s)
self.assertIsInstance(b, six.binary_type)

View File

@ -18,6 +18,7 @@ import uuid
import msgpack
from oslo_utils import timeutils
import six
from six.moves import urllib
from keystone.common import fernet_utils
@ -28,7 +29,7 @@ from keystone.federation import constants as federation_constants
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import database
from keystone.token import provider
from keystone.token.providers import common
from keystone.token.providers import fernet
from keystone.token.providers.fernet import token_formatters
@ -309,6 +310,13 @@ class TestPayloads(unit.TestCase):
return self.assertCloseEnoughForGovernmentWork(exp_time, actual_time,
delta=1e-05)
def test_strings_can_be_converted_to_bytes(self):
s = common.random_urlsafe_str()
self.assertIsInstance(s, six.text_type)
b = token_formatters.BasePayload.random_urlsafe_str_to_bytes(s)
self.assertIsInstance(b, six.binary_type)
def test_uuid_hex_to_byte_conversions(self):
payload_cls = token_formatters.BasePayload
@ -359,7 +367,7 @@ class TestPayloads(unit.TestCase):
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)
exp_audit_ids = [provider.random_urlsafe_str()]
exp_audit_ids = [common.random_urlsafe_str()]
payload = payload_class.assemble(
exp_user_id, exp_methods, exp_project_id, exp_domain_id,

View File

@ -29,8 +29,7 @@ import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.models import token_model
from keystone.token import provider
from keystone.token import providers
from keystone.token.providers import common
CONF = keystone.conf.CONF
@ -188,7 +187,7 @@ class Auth(controller.V2Controller):
# v3_to_v2_token, because federated tokens aren't supported by
# v2.0 (the same applies to OAuth tokens, domain-scoped tokens,
# etc..).
v2_helper = providers.common.V2TokenDataHelper()
v2_helper = common.V2TokenDataHelper()
v2_helper.v3_to_v2_token(v3_token_data, old_token)
token_model_ref = token_model.KeystoneToken(
token_id=old_token,
@ -321,7 +320,7 @@ class Auth(controller.V2Controller):
tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref(
user_id, tenant_id)
expiry = provider.default_expire_time()
expiry = common.default_expire_time()
bind = None
audit_id = None
return (user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id)
@ -348,7 +347,7 @@ class Auth(controller.V2Controller):
tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref(
user_id, tenant_id)
expiry = provider.default_expire_time()
expiry = common.default_expire_time()
bind = None
if ('kerberos' in CONF.token.bind and
request.environ.get('AUTH_TYPE', '').lower() == 'negotiate'):
@ -439,7 +438,7 @@ class Auth(controller.V2Controller):
"""
v3_token_response = self.token_provider_api.validate_token(token_id)
v2_helper = providers.common.V2TokenDataHelper()
v2_helper = common.V2TokenDataHelper()
token = v2_helper.v3_to_v2_token(v3_token_response, token_id)
belongs_to = request.params.get('belongsTo')
if belongs_to:
@ -458,7 +457,7 @@ class Auth(controller.V2Controller):
"""
# TODO(ayoung) validate against revocation API
v3_token_response = self.token_provider_api.validate_token(token_id)
v2_helper = providers.common.V2TokenDataHelper()
v2_helper = common.V2TokenDataHelper()
token = v2_helper.v3_to_v2_token(v3_token_response, token_id)
belongs_to = request.params.get('belongsTo')
if belongs_to:

View File

@ -27,7 +27,7 @@ import keystone.conf
from keystone import exception
from keystone.i18n import _, _LE, _LW
from keystone import token
from keystone.token import provider
from keystone.token.providers import common
CONF = keystone.conf.CONF
@ -109,7 +109,7 @@ class Token(token.persistence.TokenDriverBase):
data_copy = copy.deepcopy(data)
ptk = self._prefix_token_id(token_id)
if not data_copy.get('expires'):
data_copy['expires'] = provider.default_expire_time()
data_copy['expires'] = common.default_expire_time()
if not data_copy.get('user_id'):
data_copy['user_id'] = data_copy['user']['id']

View File

@ -23,7 +23,7 @@ import keystone.conf
from keystone import exception
from keystone.i18n import _LI
from keystone import token
from keystone.token import provider
from keystone.token.providers import common
CONF = keystone.conf.CONF
@ -95,7 +95,7 @@ class Token(token.persistence.TokenDriverBase):
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
if not data_copy.get('expires'):
data_copy['expires'] = provider.default_expire_time()
data_copy['expires'] = common.default_expire_time()
if not data_copy.get('user_id'):
data_copy['user_id'] = data_copy['user']['id']

View File

@ -14,10 +14,7 @@
"""Token provider interface."""
import base64
import datetime
import sys
import uuid
from oslo_log import log
from oslo_utils import timeutils
@ -53,76 +50,6 @@ V3 = token_model.V3
VERSIONS = token_model.VERSIONS
def base64_encode(s):
"""Encode a URL-safe string.
:type s: six.text_type
:rtype: six.text_type
"""
# urlsafe_b64encode() returns six.binary_type so need to convert to
# six.text_type, might as well do it before stripping.
return base64.urlsafe_b64encode(s).decode('utf-8').rstrip('=')
def random_urlsafe_str():
"""Generate a random URL-safe string.
:rtype: six.text_type
"""
# chop the padding (==) off the end of the encoding to save space
return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2].decode('utf-8')
def random_urlsafe_str_to_bytes(s):
"""Convert a string from :func:`random_urlsafe_str()` to six.binary_type.
:type s: six.text_type
:rtype: six.binary_type
"""
# urlsafe_b64decode() requires str, unicode isn't accepted.
s = str(s)
# restore the padding (==) at the end of the string
return base64.urlsafe_b64decode(s + '==')
def default_expire_time():
"""Determine when a fresh token should expire.
Expiration time varies based on configuration (see ``[token] expiration``).
:returns: a naive UTC datetime.datetime object
"""
expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
expires_at = timeutils.utcnow() + expire_delta
return expires_at.replace(microsecond=0)
def audit_info(parent_audit_id):
"""Build the audit data for a token.
If ``parent_audit_id`` is None, the list will be one element in length
containing a newly generated audit_id.
If ``parent_audit_id`` is supplied, the list will be two elements in length
containing a newly generated audit_id and the ``parent_audit_id``. The
``parent_audit_id`` will always be element index 1 in the resulting
list.
:param parent_audit_id: the audit of the original token in the chain
:type parent_audit_id: str
:returns: Keystone token audit data
"""
audit_id = random_urlsafe_str()
if parent_audit_id is not None:
return [audit_id, parent_audit_id]
return [audit_id]
@dependency.provider('token_provider_api')
@dependency.requires('assignment_api', 'revoke_api')
class Manager(manager.Manager):

View File

@ -12,8 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import
import base64
import datetime
import uuid
from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six
from six.moves.urllib import parse
@ -24,8 +31,7 @@ import keystone.conf
from keystone import exception
from keystone.federation import constants as federation_constants
from keystone.i18n import _
from keystone import token
from keystone.token import provider
from keystone.models import token_model
from keystone.token.providers import base
@ -33,6 +39,50 @@ LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF
def default_expire_time():
"""Determine when a fresh token should expire.
Expiration time varies based on configuration (see ``[token] expiration``).
:returns: a naive UTC datetime.datetime object
"""
expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
expires_at = timeutils.utcnow() + expire_delta
return expires_at.replace(microsecond=0)
def random_urlsafe_str():
"""Generate a random URL-safe string.
:rtype: six.text_type
"""
# chop the padding (==) off the end of the encoding to save space
return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2].decode('utf-8')
def build_audit_info(parent_audit_id=None):
"""Build the audit data for a token.
If ``parent_audit_id`` is None, the list will be one element in length
containing a newly generated audit_id.
If ``parent_audit_id`` is supplied, the list will be two elements in length
containing a newly generated audit_id and the ``parent_audit_id``. The
``parent_audit_id`` will always be element index 1 in the resulting
list.
:param parent_audit_id: the audit of the original token in the chain
:type parent_audit_id: str
:returns: Keystone token audit data
"""
audit_id = random_urlsafe_str()
if parent_audit_id is not None:
return [audit_id, parent_audit_id]
return [audit_id]
@dependency.requires('catalog_api', 'resource_api', 'assignment_api',
'trust_api', 'identity_api')
class V2TokenDataHelper(object):
@ -165,7 +215,7 @@ class V2TokenDataHelper(object):
metadata_ref = token_ref['metadata']
if roles_ref is None:
roles_ref = []
expires = token_ref.get('expires', provider.default_expire_time())
expires = token_ref.get('expires', default_expire_time())
if expires is not None:
if not isinstance(expires, six.text_type):
expires = utils.isotime(expires)
@ -177,7 +227,7 @@ class V2TokenDataHelper(object):
audit_info = token_audit
if audit_info is None:
audit_info = provider.audit_info(token_ref.get('parent_audit_id'))
audit_info = build_audit_info(token_ref.get('parent_audit_id'))
o = {'access': {'token': {'id': token_ref['id'],
'expires': expires,
@ -550,7 +600,7 @@ class V3TokenDataHelper(object):
def _populate_token_dates(self, token_data, expires=None, issued_at=None):
if not expires:
expires = provider.default_expire_time()
expires = default_expire_time()
if not isinstance(expires, six.string_types):
expires = utils.isotime(expires, subsecond=True)
token_data['expires_at'] = expires
@ -559,7 +609,7 @@ class V3TokenDataHelper(object):
def _populate_audit_info(self, token_data, audit_info=None):
if audit_info is None or isinstance(audit_info, six.string_types):
token_data['audit_ids'] = provider.audit_info(audit_info)
token_data['audit_ids'] = build_audit_info(audit_info)
elif isinstance(audit_info, list):
token_data['audit_ids'] = audit_info
else:
@ -612,16 +662,16 @@ class BaseProvider(base.Provider):
def get_token_version(self, token_data):
if token_data and isinstance(token_data, dict):
if 'token_version' in token_data:
if token_data['token_version'] in token.provider.VERSIONS:
if token_data['token_version'] in token_model.VERSIONS:
return token_data['token_version']
# FIXME(morganfainberg): deprecate the following logic in future
# revisions. It is better to just specify the token_version in
# the token_data itself. This way we can support future versions
# that might have the same fields.
if 'access' in token_data:
return token.provider.V2
return token_model.V2
if 'token' in token_data and 'methods' in token_data['token']:
return token.provider.V3
return token_model.V3
raise exception.UnsupportedTokenVersionException()
def issue_v2_token(self, token_ref, roles_ref=None,

View File

@ -29,7 +29,6 @@ from keystone.common import utils as ks_utils
import keystone.conf
from keystone import exception
from keystone.i18n import _, _LI
from keystone.token import provider
CONF = keystone.conf.CONF
@ -333,6 +332,32 @@ class BasePayload(object):
# federation)
return (False, value)
@classmethod
def base64_encode(cls, s):
"""Encode a URL-safe string.
:type s: six.text_type
:rtype: six.text_type
"""
# urlsafe_b64encode() returns six.binary_type so need to convert to
# six.text_type, might as well do it before stripping.
return base64.urlsafe_b64encode(s).decode('utf-8').rstrip('=')
@classmethod
def random_urlsafe_str_to_bytes(cls, s):
"""Convert a string from :func:`random_urlsafe_str()` to six.binary_type.
:type s: six.text_type
:rtype: six.binary_type
"""
# urlsafe_b64decode() requires str, unicode isn't accepted.
s = str(s)
# restore the padding (==) at the end of the string
return base64.urlsafe_b64decode(s + '==')
class UnscopedPayload(BasePayload):
version = 0
@ -347,7 +372,7 @@ class UnscopedPayload(BasePayload):
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)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, expires_at_int, b_audit_ids)
@ -358,7 +383,7 @@ class UnscopedPayload(BasePayload):
user_id = cls.convert_uuid_bytes_to_hex(user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
expires_at_str = cls._convert_float_to_time_string(payload[2])
audit_ids = list(map(provider.base64_encode, payload[3]))
audit_ids = list(map(cls.base64_encode, payload[3]))
project_id = None
domain_id = None
trust_id = None
@ -389,7 +414,7 @@ class DomainScopedPayload(BasePayload):
else:
raise
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids)
@ -408,7 +433,7 @@ class DomainScopedPayload(BasePayload):
else:
raise
expires_at_str = cls._convert_float_to_time_string(payload[3])
audit_ids = list(map(provider.base64_encode, payload[4]))
audit_ids = list(map(cls.base64_encode, payload[4]))
project_id = None
trust_id = None
federated_info = None
@ -431,7 +456,7 @@ class ProjectScopedPayload(BasePayload):
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,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)
@ -445,7 +470,7 @@ class ProjectScopedPayload(BasePayload):
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(provider.base64_encode, payload[4]))
audit_ids = list(map(cls.base64_encode, payload[4]))
domain_id = None
trust_id = None
federated_info = None
@ -469,7 +494,7 @@ class TrustScopedPayload(BasePayload):
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id)
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
@ -485,7 +510,7 @@ class TrustScopedPayload(BasePayload):
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(provider.base64_encode, payload[4]))
audit_ids = list(map(cls.base64_encode, payload[4]))
trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
domain_id = None
federated_info = None
@ -523,7 +548,7 @@ class FederatedUnscopedPayload(BasePayload):
federated_info['idp_id'])
protocol_id = federated_info['protocol_id']
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id,
@ -545,7 +570,7 @@ class FederatedUnscopedPayload(BasePayload):
if isinstance(protocol_id, six.binary_type):
protocol_id = protocol_id.decode('utf-8')
expires_at_str = cls._convert_float_to_time_string(payload[5])
audit_ids = list(map(provider.base64_encode, payload[6]))
audit_ids = list(map(cls.base64_encode, payload[6]))
federated_info = dict(group_ids=group_ids, idp_id=idp_id,
protocol_id=protocol_id)
project_id = None
@ -572,7 +597,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
federated_info['idp_id'])
protocol_id = federated_info['protocol_id']
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_scope_id, b_group_ids, b_idp_id,
@ -599,7 +624,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
protocol_id = payload[5]
expires_at_str = cls._convert_float_to_time_string(payload[6])
audit_ids = list(map(provider.base64_encode, payload[7]))
audit_ids = list(map(cls.base64_encode, payload[7]))
federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
group_ids=group_ids)
trust_id = None
@ -638,7 +663,7 @@ class OauthScopedPayload(BasePayload):
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,
b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
audit_ids))
b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes(
access_token_id)
@ -658,7 +683,7 @@ class OauthScopedPayload(BasePayload):
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]))
audit_ids = list(map(cls.base64_encode, payload[5]))
domain_id = None
trust_id = None
federated_info = None