diff options
author | Lance Bragstad <lbragstad@gmail.com> | 2018-02-16 20:11:45 +0000 |
---|---|---|
committer | Lance Bragstad <lbragstad@gmail.com> | 2018-07-13 14:45:56 +0000 |
commit | b47e84dac185adcc3e068237fb4cd285c1ae5583 (patch) | |
tree | 5768f9bc19f76d0f4d3105bd5b2cd71b66e9c223 /keystone/token/provider.py | |
parent | 693a86f2a17c0d7871e5700257942badd8090533 (diff) | |
download | keystone-b47e84dac185adcc3e068237fb4cd285c1ae5583.tar.gz |
Simplify the token provider API
Since we're no longer supporting persistent tokens in tree and we
removed the uuid token provider, it's the perfect time to clean up a
good amount of confusing technical debt.
The token provider API is historically known for being confusing.
This is mainly because the reference that is intended to be returned
to the user is modified all up and down the API. Different parts of
the API use the reference to invoke call hooks in other method making
the code hard to debug. In order to fully understand how tokens are
built, you need to understand where and how tokens are modified by
different layers of the API according to a specific contract of the
authentication API. Another big problem is that it couples the actual
reference of how a token looks too closely to the business logic for
tokens. Which means you have to write a ton of code if you ever want a
token to look differently, like you would if you wanted to support a
new API version.
A token should be an object that the managers and controllers can
query and reason about. From there they should be able to build token
responses accordingly. This will make the actual token provider API
much simpler because it needs to know less about API contracts that
are the responsibility of the controllers. This should lead to simpler
interfaces when new token providers are added, or maintained out of
tree. This also makes it less likely for APIs to behave differently
based on what token provider is configured by being explicitly
building the token reference in one place.
This commit ports the token business logic out of the
keystone.token.providers.common module and into a dedicated token
object, or model. This will result in a cleaner interface between the
token providers and the token provider API. A subsequent patch will
remove the unused code across the token provider API.
Partial-Bug: 1778945
Change-Id: If9ded94e65bacb0d06f5225bb36f659dc7bb8355
Diffstat (limited to 'keystone/token/provider.py')
-rw-r--r-- | keystone/token/provider.py | 160 |
1 files changed, 124 insertions, 36 deletions
diff --git a/keystone/token/provider.py b/keystone/token/provider.py index 71b499b4d..233563ee8 100644 --- a/keystone/token/provider.py +++ b/keystone/token/provider.py @@ -14,16 +14,21 @@ """Token provider interface.""" +import base64 import datetime +import uuid from oslo_log import log from oslo_utils import timeutils +import six from keystone.common import cache from keystone.common import manager from keystone.common import provider_api +from keystone.common import utils import keystone.conf from keystone import exception +from keystone.federation import constants from keystone.i18n import _ from keystone.models import token_model from keystone import notifications @@ -47,6 +52,29 @@ V3 = token_model.V3 VERSIONS = token_model.VERSIONS +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') + + class Manager(manager.Manager): """Default pivot point for the token provider backend. @@ -101,11 +129,7 @@ class Manager(manager.Manager): TOKENS_REGION.invalidate() def check_revocation_v3(self, token): - try: - token_data = token['token'] - except KeyError: - raise exception.TokenNotFound(_('Failed to validate token')) - token_values = self.revoke_api.model.build_token_values(token_data) + token_values = self.revoke_api.model.build_token_values(token) PROVIDERS.revoke_api.check_token(token_values) def check_revocation(self, token): @@ -116,31 +140,48 @@ class Manager(manager.Manager): raise exception.TokenNotFound(_('No token in the request')) try: - token_ref = self._validate_token(token_id) - self._is_valid_token(token_ref, window_seconds=window_seconds) - return token_ref + token = self._validate_token(token_id) + self._is_valid_token(token, window_seconds=window_seconds) + return token except exception.Unauthorized as e: LOG.debug('Unable to validate token: %s', e) raise exception.TokenNotFound(token_id=token_id) @MEMOIZE_TOKENS def _validate_token(self, token_id): - return self.driver.validate_token(token_id) + (user_id, methods, audit_ids, system, domain_id, + project_id, trust_id, federated_group_ids, identity_provider_id, + protocol_id, access_token_id, app_cred_id, issued_at, + expires_at) = self.driver.validate_token(token_id) + + token = token_model.TokenModel() + token.user_id = user_id + token.methods = methods + if len(audit_ids) > 1: + token.parent_audit_id = audit_ids.pop() + token.audit_id = audit_ids.pop() + token.system = system + token.domain_id = domain_id + token.project_id = project_id + token.trust_id = trust_id + token.access_token_id = access_token_id + token.application_credential_id = app_cred_id + token.expires_at = expires_at + if federated_group_ids: + token.is_federated = True + token.identity_provider_id = identity_provider_id + token.protocol_id = protocol_id + token.federated_groups = federated_group_ids + + token.mint(token_id, issued_at) + return token def _is_valid_token(self, token, window_seconds=0): """Verify the token is valid format and has not expired.""" current_time = timeutils.normalize_time(timeutils.utcnow()) try: - # Get the data we need from the correct location (V2 and V3 tokens - # differ in structure, Try V3 first, fall back to V2 second) - token_data = token.get('token', token.get('access')) - expires_at = token_data.get('expires_at', - token_data.get('expires')) - if not expires_at: - expires_at = token_data['token']['expires'] - - expiry = timeutils.parse_isotime(expires_at) + expiry = timeutils.parse_isotime(token.expires_at) expiry = timeutils.normalize_time(expiry) # add a window in which you can fetch a token beyond expiry @@ -159,24 +200,73 @@ class Manager(manager.Manager): raise exception.TokenNotFound(_('Failed to validate token')) def issue_token(self, user_id, method_names, expires_at=None, - system=None, project_id=None, is_domain=False, - domain_id=None, auth_context=None, trust=None, - app_cred_id=None, include_catalog=True, + system=None, project_id=None, domain_id=None, + auth_context=None, trust_id=None, app_cred_id=None, parent_audit_id=None): - token_id, token_data = self.driver.issue_token( - user_id, method_names, expires_at=expires_at, - system=system, project_id=project_id, - domain_id=domain_id, auth_context=auth_context, trust=trust, - app_cred_id=app_cred_id, include_catalog=include_catalog, - parent_audit_id=parent_audit_id) + # NOTE(lbragstad): Check if the token provider being used actually + # supports bind authentication methods before proceeding. + if auth_context and auth_context.get('bind'): + if not self.driver._supports_bind_authentication: + raise exception.NotImplemented(_( + 'The configured token provider does not support bind ' + 'authentication.')) + + # NOTE(lbragstad): Grab a blank token object and use composition to + # build the token according to the authentication and authorization + # context. This cuts down on the amount of logic we have to stuff into + # the TokenModel's __init__() method. + token = token_model.TokenModel() + token.methods = method_names + token.system = system + token.domain_id = domain_id + token.project_id = project_id + token.trust_id = trust_id + token.application_credential_id = app_cred_id + token.audit_id = random_urlsafe_str() + token.parent_audit_id = parent_audit_id + + if auth_context: + if constants.IDENTITY_PROVIDER in auth_context: + token.is_federated = True + token.protocol_id = auth_context[constants.PROTOCOL] + idp_id = auth_context[constants.IDENTITY_PROVIDER] + if isinstance(idp_id, bytes): + idp_id = idp_id.decode('utf-8') + token.identity_provider_id = idp_id + token.user_id = auth_context['user_id'] + token.federated_groups = [ + {'id': group} for group in auth_context['group_ids'] + ] + + if 'access_token_id' in auth_context: + token.access_token_id = auth_context['access_token_id'] + + if not token.user_id: + token.user_id = user_id + + token.user_domain_id = token.user['domain_id'] + + if isinstance(expires_at, datetime.datetime): + token.expires_at = utils.isotime(expires_at, subsecond=True) + if isinstance(expires_at, six.string_types): + token.expires_at = expires_at + elif not expires_at: + token.expires_at = utils.isotime( + default_expire_time(), subsecond=True + ) + + token_id, issued_at = self.driver.generate_id_and_issued_at(token) + token.mint(token_id, issued_at) + + # cache the token object and with ID if CONF.token.cache_on_issue: # NOTE(amakarov): here and above TOKENS_REGION is to be passed # to serve as required positional "self" argument. It's ignored, # so I've put it here for convenience - any placeholder is fine. - self._validate_token.set(token_data, TOKENS_REGION, token_id) + self._validate_token.set(token, self, token.id) - return token_id, token_data + return token def invalidate_individual_token_cache(self, token_id): # NOTE(morganfainberg): invalidate takes the exact same arguments as @@ -191,20 +281,18 @@ class Manager(manager.Manager): self._validate_token.invalidate(self, token_id) def revoke_token(self, token_id, revoke_chain=False): - token_ref = token_model.KeystoneToken( - token_id=token_id, - token_data=self.validate_token(token_id)) + token = self.validate_token(token_id) - project_id = token_ref.project_id if token_ref.project_scoped else None - domain_id = token_ref.domain_id if token_ref.domain_scoped else None + project_id = token.project_id if token.project_scoped else None + domain_id = token.domain_id if token.domain_scoped else None if revoke_chain: PROVIDERS.revoke_api.revoke_by_audit_chain_id( - token_ref.audit_chain_id, project_id=project_id, + token.parent_audit_id, project_id=project_id, domain_id=domain_id ) else: - PROVIDERS.revoke_api.revoke_by_audit_id(token_ref.audit_id) + PROVIDERS.revoke_api.revoke_by_audit_id(token.audit_id) # FIXME(morganfainberg): Does this cache actually need to be # invalidated? We maintain a cached revocation list, which should be |