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
This commit is contained in:
Lance Bragstad 2018-02-16 20:11:45 +00:00
parent 693a86f2a1
commit b47e84dac1
21 changed files with 630 additions and 609 deletions

View File

@ -121,6 +121,7 @@ class Auth(controller.V3Controller):
(domain_id, project_id, trust, unscoped, system) = (
auth_info.get_scope()
)
trust_id = trust.get('id') if trust else None
# NOTE(notmorgan): only methods that actually run and succeed will
# be in the auth_context['method_names'] list. Do not blindly take
@ -144,21 +145,21 @@ class Auth(controller.V3Controller):
expires_at = auth_context.get('expires_at')
token_audit_id = auth_context.get('audit_id')
is_domain = auth_context.get('is_domain')
(token_id, token_data) = PROVIDERS.token_provider_api.issue_token(
token = PROVIDERS.token_provider_api.issue_token(
auth_context['user_id'], method_names, expires_at=expires_at,
system=system, project_id=project_id,
is_domain=is_domain, domain_id=domain_id,
auth_context=auth_context, trust=trust,
app_cred_id=app_cred_id, include_catalog=include_catalog,
parent_audit_id=token_audit_id)
system=system, project_id=project_id, domain_id=domain_id,
auth_context=auth_context, trust_id=trust_id,
app_cred_id=app_cred_id, parent_audit_id=token_audit_id)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
# NOTE(wanghong): We consume a trust use only when we are using
# trusts and have successfully issued a token.
if trust:
PROVIDERS.trust_api.consume_use(trust['id'])
PROVIDERS.trust_api.consume_use(token.trust_id)
return render_token_data_response(token_id, token_data,
return render_token_data_response(token.id, token_reference,
created=True)
except exception.TrustNotFound as e:
LOG.warning(six.text_type(e))
@ -311,12 +312,17 @@ class Auth(controller.V3Controller):
def check_token(self, request):
token_id = request.subject_token
window_seconds = authorization.token_validation_window(request)
token_data = PROVIDERS.token_provider_api.validate_token(
include_catalog = 'nocatalog' not in request.params
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_seconds)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
# NOTE(morganfainberg): The code in
# ``keystone.common.wsgi.render_response`` will remove the content
# body.
return render_token_data_response(token_id, token_data)
return render_token_data_response(token.id, token_reference)
@controller.protected()
def revoke_token(self, request):
@ -327,11 +333,14 @@ class Auth(controller.V3Controller):
token_id = request.subject_token
window_seconds = authorization.token_validation_window(request)
include_catalog = 'nocatalog' not in request.params
token_data = PROVIDERS.token_provider_api.validate_token(
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_seconds)
if not include_catalog and 'catalog' in token_data['token']:
del token_data['token']['catalog']
return render_token_data_response(token_id, token_data)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
return render_token_data_response(token.id, token_reference)
@controller.protected()
def revocation_list(self, request):

View File

@ -72,14 +72,16 @@ class Mapped(base.AuthMethodHandler):
response_data=response_data)
def handle_scoped_token(request, token_ref, federation_api, identity_api):
def handle_scoped_token(request, token, federation_api, identity_api):
response_data = {}
utils.validate_expiration(token_ref)
token_audit_id = token_ref.audit_id
identity_provider = token_ref.federation_idp_id
protocol = token_ref.federation_protocol_id
user_id = token_ref.user_id
group_ids = token_ref.federation_group_ids
utils.validate_expiration(token)
token_audit_id = token.audit_id
identity_provider = token.identity_provider_id
protocol = token.protocol_id
user_id = token.user_id
group_ids = []
for group_dict in token.federated_groups:
group_ids.append(group_dict['id'])
send_notification = functools.partial(
notifications.send_saml_audit_notification, 'authenticate',
request, user_id, group_ids, identity_provider, protocol,

View File

@ -21,7 +21,6 @@ from keystone.common import provider_api
import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.models import token_model
LOG = log.getLogger(__name__)
@ -34,35 +33,32 @@ class Token(base.AuthMethodHandler):
def _get_token_ref(self, auth_payload):
token_id = auth_payload['id']
response = PROVIDERS.token_provider_api.validate_token(token_id)
return token_model.KeystoneToken(token_id=token_id,
token_data=response)
return PROVIDERS.token_provider_api.validate_token(token_id)
def authenticate(self, request, auth_payload):
if 'id' not in auth_payload:
raise exception.ValidationError(attribute='id',
target='token')
token_ref = self._get_token_ref(auth_payload)
if token_ref.is_federated_user and PROVIDERS.federation_api:
token = self._get_token_ref(auth_payload)
if token.is_federated and PROVIDERS.federation_api:
response_data = mapped.handle_scoped_token(
request, token_ref, PROVIDERS.federation_api,
request, token, PROVIDERS.federation_api,
PROVIDERS.identity_api
)
else:
response_data = token_authenticate(request,
token_ref)
response_data = token_authenticate(request, token)
# NOTE(notmorgan): The Token auth method is *very* special and sets the
# previous values to the method_names. This is because it can be used
# for re-scoping and we want to maintain the values. Most
# AuthMethodHandlers do no such thing and this is not required.
response_data.setdefault('method_names', []).extend(token_ref.methods)
response_data.setdefault('method_names', []).extend(token.methods)
return base.AuthHandlerResponse(status=True, response_body=None,
response_data=response_data)
def token_authenticate(request, token_ref):
def token_authenticate(request, token):
response_data = {}
try:
@ -78,17 +74,17 @@ def token_authenticate(request, token_ref):
'scope', {}
)
if token_ref.oauth_scoped:
if token.oauth_scoped:
raise exception.ForbiddenAction(
action=_(
'Using OAuth-scoped token to create another token. '
'Create a new OAuth-scoped token instead'))
elif token_ref.trust_scoped:
elif token.trust_scoped:
raise exception.ForbiddenAction(
action=_(
'Using trust-scoped token to create another token. '
'Create a new trust-scoped token instead'))
elif token_ref.system_scoped and (project_scoped or domain_scoped):
elif token.system_scoped and (project_scoped or domain_scoped):
raise exception.ForbiddenAction(
action=_(
'Using a system-scoped token to create a project-scoped '
@ -98,7 +94,7 @@ def token_authenticate(request, token_ref):
if not CONF.token.allow_rescope_scoped_token:
# Do not allow conversion from scoped tokens.
if token_ref.project_scoped or token_ref.domain_scoped:
if token.project_scoped or token.domain_scoped:
raise exception.ForbiddenAction(
action=_('rescope a scoped token'))
@ -109,7 +105,7 @@ def token_authenticate(request, token_ref):
# a token that has not been rescoped) or the audit_chain id (in
# the case of a token that has been rescoped).
try:
token_audit_id = token_ref.get('audit_ids', [])[-1]
token_audit_id = token.parent_audit_id or token.audit_id
except IndexError:
# NOTE(morganfainberg): In the case this is a token that was
# issued prior to audit id existing, the chain is not tracked.
@ -119,13 +115,13 @@ def token_authenticate(request, token_ref):
# token expiration time is maintained in the new token. Not doing this
# would make it possible for a user to continuously bump token
# expiration through token rescoping without proving their identity.
response_data.setdefault('expires_at', token_ref.expires)
response_data.setdefault('expires_at', token.expires_at)
response_data['audit_id'] = token_audit_id
response_data.setdefault('user_id', token_ref.user_id)
response_data.setdefault('user_id', token.user_id)
# TODO(morganfainberg: determine if token 'extras' can be removed
# from the response_data
response_data.setdefault('extras', {}).update(
token_ref.get('extras', {}))
# response_data.setdefault('extras', {}).update(
# token.get('extras', {}))
return response_data

View File

@ -22,7 +22,6 @@ from keystone.common.policies import base as pol_base
from keystone.common import utils
from keystone import conf
from keystone import exception
from keystone.models import token_model
# Header used to transmit the auth token
@ -90,17 +89,14 @@ def _handle_subject_token_id(self, request, policy_dict):
if request.subject_token is not None:
window_seconds = token_validation_window(request)
token_ref = token_model.KeystoneToken(
token_id=request.subject_token,
token_data=self.token_provider_api.validate_token(
request.subject_token,
window_seconds=window_seconds))
token = self.token_provider_api.validate_token(
request.subject_token, window_seconds=window_seconds
)
policy_dict.setdefault('target', {})
policy_dict['target'].setdefault(self.member_name, {})
policy_dict['target'][self.member_name]['user_id'] = (
token_ref.user_id)
policy_dict['target'][self.member_name]['user_id'] = (token.user_id)
try:
user_domain_id = token_ref.user_domain_id
user_domain_id = token.user_domain['id']
except exception.UnexpectedError:
user_domain_id = None
if user_domain_id:

View File

@ -31,6 +31,7 @@ from keystone.i18n import _
LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
def protected(callback=None):
@ -122,6 +123,139 @@ def protected_wrapper(self, f, check_function, request, filter_attr,
check_function(self, request, prep_info, *args, **kwargs)
# FIXME(lbragstad): Find a better home for this... I put there here since it's
# needed across a couple different controller (keystone/auth/controllers.py and
# keystone/contrib/ec2/controllers.py both need it). This is technically an
# opinion of how a token should look according to the V3 API contract. My
# thought was to try and work this into a view of a token associated to the V3
# controller logic somewhere.
def render_token_response_from_model(token, include_catalog=True):
token_reference = {
'token': {
'methods': token.methods,
'user': {
'domain': {
'id': token.user_domain['id'],
'name': token.user_domain['name']
},
'id': token.user_id,
'name': token.user['name'],
'password_expires_at': token.user[
'password_expires_at'
]
},
'audit_ids': token.audit_ids,
'expires_at': token.expires_at,
'issued_at': token.issued_at,
}
}
if token.system_scoped:
token_reference['token']['roles'] = token.roles
token_reference['token']['system'] = {'all': True}
elif token.domain_scoped:
token_reference['token']['domain'] = {
'id': token.domain['id'],
'name': token.domain['name']
}
token_reference['token']['roles'] = token.roles
elif token.trust_scoped:
token_reference['token']['OS-TRUST:trust'] = {
'id': token.trust_id,
'trustor_user': {'id': token.trustor['id']},
'trustee_user': {'id': token.trustee['id']},
'impersonation': token.trust['impersonation']
}
token_reference['token']['project'] = {
'domain': {
'id': token.project_domain['id'],
'name': token.project_domain['name']
},
'id': token.trust_project['id'],
'name': token.trust_project['name']
}
if token.trust.get('impersonation'):
trustor_domain = PROVIDERS.resource_api.get_domain(
token.trustor['domain_id']
)
token_reference['token']['user'] = {
'domain': {
'id': trustor_domain['id'],
'name': trustor_domain['name']
},
'id': token.trustor['id'],
'name': token.trustor['name'],
'password_expires_at': token.trustor[
'password_expires_at'
]
}
token_reference['token']['roles'] = token.roles
elif token.project_scoped:
token_reference['token']['project'] = {
'domain': {
'id': token.project_domain['id'],
'name': token.project_domain['name']
},
'id': token.project['id'],
'name': token.project['name']
}
token_reference['token']['is_domain'] = token.project.get(
'is_domain', False
)
token_reference['token']['roles'] = token.roles
ap_name = CONF.resource.admin_project_name
ap_domain_name = CONF.resource.admin_project_domain_name
if ap_name and ap_domain_name:
is_ap = (
token.project['name'] == ap_name and
ap_domain_name == token.project_domain['name']
)
token_reference['token']['is_admin_project'] = is_ap
if include_catalog and not token.unscoped:
user_id = token.user_id
if token.trust_id:
user_id = token.trust['trustor_user_id']
catalog = PROVIDERS.catalog_api.get_v3_catalog(
user_id, token.project_id
)
token_reference['token']['catalog'] = catalog
sps = PROVIDERS.federation_api.get_enabled_service_providers()
if sps:
token_reference['token']['service_providers'] = sps
if token.is_federated:
PROVIDERS.federation_api.get_idp(token.identity_provider_id)
federated_dict = {}
federated_dict['groups'] = token.federated_groups
federated_dict['identity_provider'] = {
'id': token.identity_provider_id
}
federated_dict['protocol'] = {'id': token.protocol_id}
token_reference['token']['user']['OS-FEDERATION'] = (
federated_dict
)
token_reference['token']['user']['domain'] = {
'id': 'Federated', 'name': 'Federated'
}
del token_reference['token']['user']['password_expires_at']
if token.access_token_id:
token_reference['token']['OS-OAUTH1'] = {
'access_token_id': token.access_token_id,
'consumer_id': token.access_token['consumer_id']
}
if token.application_credential_id:
key = 'application_credential'
token_reference['token'][key] = {}
token_reference['token'][key]['id'] = (
token.application_credential['id']
)
token_reference['token'][key]['name'] = (
token.application_credential['name']
)
restricted = not token.application_credential['unrestricted']
token_reference['token'][key]['restricted'] = restricted
return token_reference
class V3Controller(provider_api.ProviderAPIMixin, wsgi.Application):
"""Base controller class for Identity API v3.

View File

@ -25,7 +25,6 @@ from keystone.common import utils
import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.models import token_model
CONF = keystone.conf.CONF
@ -189,16 +188,15 @@ class RBACEnforcer(object):
default=False))
if allow_expired:
window_seconds = CONF.token.allow_expired_window
token_ref = token_model.KeystoneToken(
token_id=subject_token,
token_data=PROVIDER_APIS.token_provider_api.validate_token(
subject_token,
window_seconds=window_seconds))
token = PROVIDER_APIS.token_provider_api.validate_token(
subject_token,
window_seconds=window_seconds
)
# TODO(morgan): Expand extracted data from the subject token.
ret_dict[target] = {}
ret_dict[target]['user_id'] = token_ref.user_id
ret_dict[target]['user_id'] = token.user_id
try:
user_domain_id = token_ref.user_domain_id
user_domain_id = token.user_domain_id
except exception.UnexpectedError:
user_domain_id = None
if user_domain_id:

View File

@ -296,9 +296,11 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
method_names = ['ec2credential']
token_id, token_data = self.token_provider_api.issue_token(
user_ref['id'], method_names, project_id=project_ref['id'])
return self.render_token_data_response(token_id, token_data)
token = self.token_provider_api.issue_token(
user_ref['id'], method_names, project_id=project_ref['id']
)
token_reference = controller.render_token_response_from_model(token)
return self.render_token_data_response(token.id, token_reference)
@controller.protected(callback=_check_credential_owner_and_user_id_match)
def ec2_get_credential(self, request, user_id, credential_id):

View File

@ -32,7 +32,6 @@ from keystone.federation import idp as keystone_idp
from keystone.federation import schema
from keystone.federation import utils
from keystone.i18n import _
from keystone.models import token_model
CONF = keystone.conf.CONF
@ -369,26 +368,27 @@ class Auth(auth_controllers.Auth):
sp_url = service_provider['sp_url']
token_id = auth['identity']['token']['id']
token_data = PROVIDERS.token_provider_api.validate_token(token_id)
token_ref = token_model.KeystoneToken(token_id, token_data)
token = PROVIDERS.token_provider_api.validate_token(token_id)
if not token_ref.project_scoped:
if not token.project_scoped:
action = _('Use a project scoped token when attempting to create '
'a SAML assertion')
raise exception.ForbiddenAction(action=action)
subject = token_ref.user_name
roles = token_ref.role_names
project = token_ref.project_name
subject = token.user['name']
role_names = []
for role in token.roles:
role_names.append(role['name'])
project = token.project['name']
# NOTE(rodrigods): the domain name is necessary in order to distinguish
# between projects and users with the same name in different domains.
project_domain_name = token_ref.project_domain_name
subject_domain_name = token_ref.user_domain_name
project_domain_name = token.project_domain['name']
subject_domain_name = token.user_domain['name']
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(
issuer, sp_url, subject, subject_domain_name,
roles, project, project_domain_name)
role_names, project, project_domain_name)
return (response, service_provider)
def _build_response_headers(self, service_provider):

View File

@ -268,8 +268,11 @@ def validate_mapping_structure(ref):
raise exception.ValidationError(messages)
def validate_expiration(token_ref):
if timeutils.utcnow() > token_ref.expires:
def validate_expiration(token):
token_expiration_datetime = timeutils.normalize_time(
timeutils.parse_isotime(token.expires_at)
)
if timeutils.utcnow() > token_expiration_datetime:
raise exception.Unauthorized(_('Federation token is expired'))

View File

@ -15,6 +15,7 @@ from oslo_log import log
from keystone.common import authorization
from keystone.common import context
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import tokenless_auth
from keystone.common import wsgi
@ -43,7 +44,8 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
def fetch_token(self, token, **kwargs):
try:
return self.token_provider_api.validate_token(token)
token_model = self.token_provider_api.validate_token(token)
return controller.render_token_response_from_model(token_model)
except exception.TokenNotFound:
raise auth_token.InvalidToken(_('Could not find token'))

View File

@ -204,9 +204,9 @@ def matches(event, token_values):
return True
def build_token_values(token_data):
def build_token_values(token):
token_expires_at = timeutils.parse_isotime(token_data['expires_at'])
token_expires_at = timeutils.parse_isotime(token.expires_at)
# Trim off the microseconds because the revocation event only has
# expirations accurate to the second.
@ -215,60 +215,54 @@ def build_token_values(token_data):
token_values = {
'expires_at': timeutils.normalize_time(token_expires_at),
'issued_at': timeutils.normalize_time(
timeutils.parse_isotime(token_data['issued_at'])),
'audit_id': token_data.get('audit_ids', [None])[0],
'audit_chain_id': token_data.get('audit_ids', [None])[-1],
timeutils.parse_isotime(token.issued_at)),
'audit_id': token.audit_id,
'audit_chain_id': token.parent_audit_id,
}
user = token_data.get('user')
if user is not None:
token_values['user_id'] = user['id']
if token.user_id is not None:
token_values['user_id'] = token.user_id
# Federated users do not have a domain, be defensive and get the user
# domain set to None in the federated user case.
token_values['identity_domain_id'] = user.get('domain', {}).get('id')
token_values['identity_domain_id'] = token.user_domain['id']
else:
token_values['user_id'] = None
token_values['identity_domain_id'] = None
project = token_data.get('project', token_data.get('tenant'))
if project is not None:
token_values['project_id'] = project['id']
if token.project_id is not None:
token_values['project_id'] = token.project_id
# The domain_id of projects acting as domains is None
token_values['assignment_domain_id'] = (
project['domain']['id'] if project['domain'] else None)
token_values['assignment_domain_id'] = token.project_domain['id']
else:
token_values['project_id'] = None
domain = token_data.get('domain')
if domain is not None:
token_values['assignment_domain_id'] = domain['id']
else:
token_values['assignment_domain_id'] = None
if token.domain_id is not None:
token_values['assignment_domain_id'] = token.domain_id
else:
token_values['assignment_domain_id'] = None
role_list = []
roles = token_data.get('roles')
if roles is not None:
for role in roles:
if token.roles is not None:
for role in token.roles:
role_list.append(role['id'])
token_values['roles'] = role_list
trust = token_data.get('OS-TRUST:trust')
if trust is None:
if token.trust_scoped:
token_values['trust_id'] = token.trust['id']
token_values['trustor_id'] = token.trustor['id']
token_values['trustee_id'] = token.trustee['id']
else:
token_values['trust_id'] = None
token_values['trustor_id'] = None
token_values['trustee_id'] = None
else:
token_values['trust_id'] = trust['id']
token_values['trustor_id'] = trust['trustor_user']['id']
token_values['trustee_id'] = trust['trustee_user']['id']
oauth1 = token_data.get('OS-OAUTH1')
if oauth1 is None:
if token.oauth_scoped:
token_values['consumer_id'] = token.access_token['consumer_id']
token_values['access_token_id'] = token.access_token['id']
else:
token_values['consumer_id'] = None
token_values['access_token_id'] = None
else:
token_values['consumer_id'] = oauth1['consumer_id']
token_values['access_token_id'] = oauth1['access_token_id']
return token_values

View File

@ -22,12 +22,10 @@ import six
from keystone.common import cache
from keystone.common import provider_api
import keystone.conf
from keystone import exception
from keystone.federation import constants
from keystone.i18n import _
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs

View File

@ -25,7 +25,6 @@ from keystone.common import context
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone import exception
from keystone.models import token_model
from keystone.tests import unit
from keystone.tests.unit import rest
@ -236,19 +235,14 @@ class TestRBACEnforcerRest(_TestRBACEnforcerBase):
c.get('/v3', headers={'X-Auth-Token': token_id,
'X-Subject-Token': token_id})
token_ref = token_model.KeystoneToken(
token_id=token_id,
token_data=PROVIDER_APIS.token_provider_api.validate_token(
token_id
)
)
token = PROVIDER_APIS.token_provider_api.validate_token(token_id)
subj_token_data = (
self.enforcer._extract_subject_token_target_data())
subj_token_data = subj_token_data['token']
self.assertEqual(token_ref.user_id, subj_token_data['user_id'])
self.assertEqual(token.user_id, subj_token_data['user_id'])
self.assertIn('user', subj_token_data)
self.assertIn('domain', subj_token_data['user'])
self.assertEqual(token_ref.user_domain_id,
self.assertEqual(token.user_domain['id'],
subj_token_data['user']['domain']['id'])
def test_extract_filter_data(self):

View File

@ -20,6 +20,7 @@ from keystone.common import provider_api
from keystone.common import utils
import keystone.conf
from keystone import exception
from keystone.models import token_model
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import database
@ -451,18 +452,6 @@ class TestTokenProvider(unit.TestCase):
)
self.load_backends()
def test_get_token_version(self):
self.assertEqual(
token.provider.V3,
PROVIDERS.token_provider_api.get_token_version(SAMPLE_V3_TOKEN))
self.assertEqual(
token.provider.V3,
PROVIDERS.token_provider_api.get_token_version(
SAMPLE_V3_TOKEN_WITH_EMBEDED_VERSION))
self.assertRaises(exception.UnsupportedTokenVersionException,
PROVIDERS.token_provider_api.get_token_version,
'bogus')
def test_unsupported_token_provider(self):
self.config_fixture.config(group='token',
provider='MyProvider')
@ -470,14 +459,18 @@ class TestTokenProvider(unit.TestCase):
token.provider.Manager)
def test_provider_token_expiration_validation(self):
token = token_model.TokenModel()
token.issued_at = "2013-05-21T00:02:43.941473Z"
token.expires_at = utils.isotime(CURRENT_DATE)
self.assertRaises(exception.TokenNotFound,
PROVIDERS.token_provider_api._is_valid_token,
SAMPLE_V3_TOKEN_EXPIRED)
self.assertRaises(exception.TokenNotFound,
PROVIDERS.token_provider_api._is_valid_token,
SAMPLE_MALFORMED_TOKEN)
self.assertIsNone(
PROVIDERS.token_provider_api._is_valid_token(create_v3_token()))
token)
token = token_model.TokenModel()
token.issued_at = "2013-05-21T00:02:43.941473Z"
token.expires_at = utils.isotime(timeutils.utcnow() + FUTURE_DELTA)
self.assertIsNone(PROVIDERS.token_provider_api._is_valid_token(token))
def test_validate_v3_token_with_no_token_raises_token_not_found(self):
self.assertRaises(

View File

@ -79,25 +79,20 @@ class TestValidate(unit.TestCase):
user_ref = PROVIDERS.identity_api.create_user(user_ref)
method_names = ['password']
token_id, token_data_ = PROVIDERS.token_provider_api.issue_token(
token = PROVIDERS.token_provider_api.issue_token(
user_ref['id'], method_names)
token_data = PROVIDERS.token_provider_api.validate_token(token_id)
token = token_data['token']
self.assertIsInstance(token['audit_ids'], list)
self.assertIsInstance(token['expires_at'], str)
self.assertIsInstance(token['issued_at'], str)
self.assertEqual(method_names, token['methods'])
exp_user_info = {
'id': user_ref['id'],
'name': user_ref['name'],
'domain': {
'id': domain_ref['id'],
'name': domain_ref['name'],
},
'password_expires_at': user_ref['password_expires_at']
}
self.assertEqual(exp_user_info, token['user'])
token = PROVIDERS.token_provider_api.validate_token(token.id)
self.assertIsInstance(token.audit_ids, list)
self.assertIsInstance(token.expires_at, str)
self.assertIsInstance(token.issued_at, str)
self.assertEqual(method_names, token.methods)
self.assertEqual(user_ref['id'], token.user_id)
self.assertEqual(user_ref['name'], token.user['name'])
self.assertDictEqual(domain_ref, token.user_domain)
self.assertEqual(
user_ref['password_expires_at'], token.user['password_expires_at']
)
def test_validate_v3_token_federated_info(self):
# Check the user fields in the token result when use validate_v3_token
@ -130,23 +125,18 @@ class TestValidate(unit.TestCase):
federation_constants.PROTOCOL: protocol,
}
auth_context = auth.core.AuthContext(**auth_context_params)
token_id, token_data_ = PROVIDERS.token_provider_api.issue_token(
token = PROVIDERS.token_provider_api.issue_token(
user_ref['id'], method_names, auth_context=auth_context)
token_data = PROVIDERS.token_provider_api.validate_token(token_id)
token = token_data['token']
exp_user_info = {
'id': user_ref['id'],
'name': user_ref['name'],
'domain': {'id': CONF.federation.federated_domain_name,
'name': CONF.federation.federated_domain_name, },
federation_constants.FEDERATION: {
'groups': [{'id': group_id} for group_id in group_ids],
'identity_provider': {'id': idp_id, },
'protocol': {'id': protocol, },
},
}
self.assertDictEqual(exp_user_info, token['user'])
token = PROVIDERS.token_provider_api.validate_token(token.id)
self.assertEqual(user_ref['id'], token.user_id)
self.assertEqual(user_ref['name'], token.user['name'])
self.assertDictEqual(domain_ref, token.user_domain)
exp_group_ids = [{'id': group_id} for group_id in group_ids]
self.assertEqual(exp_group_ids, token.federated_groups)
self.assertEqual(idp_id, token.identity_provider_id)
self.assertEqual(protocol, token.protocol_id)
def test_validate_v3_token_trust(self):
# Check the trust fields in the token result when use validate_v3_token
@ -190,19 +180,15 @@ class TestValidate(unit.TestCase):
method_names = ['password']
token_id, token_data_ = PROVIDERS.token_provider_api.issue_token(
token = PROVIDERS.token_provider_api.issue_token(
user_ref['id'], method_names, project_id=project_ref['id'],
trust=trust_ref)
trust_id=trust_ref['id'])
token_data = PROVIDERS.token_provider_api.validate_token(token_id)
token = token_data['token']
exp_trust_info = {
'id': trust_ref['id'],
'impersonation': False,
'trustee_user': {'id': user_ref['id'], },
'trustor_user': {'id': trustor_user_ref['id'], },
}
self.assertEqual(exp_trust_info, token['OS-TRUST:trust'])
token = PROVIDERS.token_provider_api.validate_token(token.id)
self.assertEqual(trust_ref['id'], token.trust_id)
self.assertFalse(token.trust['impersonation'])
self.assertEqual(user_ref['id'], token.trustee['id'])
self.assertEqual(trustor_user_ref['id'], token.trustor['id'])
def test_validate_v3_token_validation_error_exc(self):
# When the token format isn't recognized, TokenNotFound is raised.
@ -303,10 +289,10 @@ class TestPayloads(unit.TestCase):
self.assertEqual(expected_time_str, actual_time_str)
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_app_cred_id=None):
exp_system=None, exp_project_id=None, exp_domain_id=None,
exp_trust_id=None, exp_federated_group_ids=None,
exp_identity_provider_id=None, exp_protocol_id=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)
@ -315,11 +301,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_app_cred_id)
exp_federated_group_ids, exp_identity_provider_id, exp_protocol_id,
exp_access_token_id, exp_app_cred_id)
(user_id, methods, system, project_id,
domain_id, expires_at, audit_ids,
trust_id, federated_info,
trust_id, federated_group_ids, identity_provider_id, protocol_id,
access_token_id, app_cred_id) = payload_class.disassemble(payload)
self.assertEqual(exp_user_id, user_id)
@ -329,15 +316,13 @@ class TestPayloads(unit.TestCase):
self.assertEqual(exp_system, system)
self.assertEqual(exp_project_id, project_id)
self.assertEqual(exp_domain_id, domain_id)
self.assertEqual(exp_federated_group_ids, federated_group_ids)
self.assertEqual(exp_identity_provider_id, identity_provider_id)
self.assertEqual(exp_protocol_id, protocol_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)
else:
self.assertIsNone(federated_info)
def test_unscoped_payload(self):
self._test_payload(token_formatters.UnscopedPayload)
@ -403,13 +388,15 @@ class TestPayloads(unit.TestCase):
exp_trust_id=uuid.uuid4().hex)
def _test_federated_payload_with_ids(self, exp_user_id, exp_group_id):
exp_federated_info = {'group_ids': [{'id': exp_group_id}],
'idp_id': uuid.uuid4().hex,
'protocol_id': uuid.uuid4().hex}
exp_federated_group_ids = [{'id': exp_group_id}]
exp_idp_id = uuid.uuid4().hex
exp_protocol_id = uuid.uuid4().hex
self._test_payload(token_formatters.FederatedUnscopedPayload,
exp_user_id=exp_user_id,
exp_federated_info=exp_federated_info)
exp_federated_group_ids=exp_federated_group_ids,
exp_identity_provider_id=exp_idp_id,
exp_protocol_id=exp_protocol_id)
def test_federated_payload_with_non_uuid_ids(self):
self._test_federated_payload_with_ids('someNonUuidUserId',
@ -420,26 +407,30 @@ class TestPayloads(unit.TestCase):
'0123456789abcdef')
def test_federated_project_scoped_payload(self):
exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}],
'idp_id': uuid.uuid4().hex,
'protocol_id': uuid.uuid4().hex}
exp_federated_group_ids = [{'id': 'someNonUuidGroupId'}]
exp_idp_id = uuid.uuid4().hex
exp_protocol_id = uuid.uuid4().hex
self._test_payload(token_formatters.FederatedProjectScopedPayload,
exp_user_id='someNonUuidUserId',
exp_methods=['token'],
exp_project_id=uuid.uuid4().hex,
exp_federated_info=exp_federated_info)
exp_federated_group_ids=exp_federated_group_ids,
exp_identity_provider_id=exp_idp_id,
exp_protocol_id=exp_protocol_id)
def test_federated_domain_scoped_payload(self):
exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}],
'idp_id': uuid.uuid4().hex,
'protocol_id': uuid.uuid4().hex}
exp_federated_group_ids = [{'id': 'someNonUuidGroupId'}]
exp_idp_id = uuid.uuid4().hex
exp_protocol_id = uuid.uuid4().hex
self._test_payload(token_formatters.FederatedDomainScopedPayload,
exp_user_id='someNonUuidUserId',
exp_methods=['token'],
exp_domain_id=uuid.uuid4().hex,
exp_federated_info=exp_federated_info)
exp_federated_group_ids=exp_federated_group_ids,
exp_identity_provider_id=exp_idp_id,
exp_protocol_id=exp_protocol_id)
def test_oauth_scoped_payload(self):
self._test_payload(token_formatters.OauthScopedPayload,

View File

@ -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

View File

@ -24,66 +24,44 @@ class Provider(object):
"""Interface description for a Token provider."""
@abc.abstractmethod
def get_token_version(self, token_data):
"""Return the version of the given token data.
def validate_token(self, token_id):
"""Validate a given token by its ID and return the token_data.
If the given token data is unrecognizable,
UnsupportedTokenVersionException is raised.
:param token_id: the unique ID of the token
:type token_id: str
:returns: token data as a tuple in the form of:
:param token_data: token_data
:type token_data: dict
:returns: token version string
:raises keystone.exception.UnsupportedTokenVersionException:
If the token version is not expected.
"""
raise exception.NotImplemented() # pragma: no cover
(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)
@abc.abstractmethod
def issue_token(self, user_id, method_names, expires_at=None,
project_id=None, domain_id=None, auth_context=None,
trust=None, include_catalog=True, parent_audit_id=None):
"""Issue a V3 Token.
``user_id`` is the unique ID of the user as a string
``methods`` a list of authentication methods used to obtain the token
``audit_ids`` a list of audit IDs for the token
``system`` a dictionary containing system scope if system-scoped
``domain_id`` the unique ID of the domain if domain-scoped
``project_id`` the unique ID of the project if project-scoped
``trust_id`` the unique identifier of the trust if trust-scoped
``federated_group_ids`` list of federated group IDs
``identity_provider_id`` unique ID of the user's identity provider
``protocol_id`` unique ID of the protocol used to obtain the token
``access_token_id`` the unique ID of the access_token for OAuth1 tokens
``app_cred_id`` the unique ID of the application credential
``issued_at`` a datetime object of when the token was minted
``expires_at`` a datetime object of when the token expires
:param user_id: identity of the user
:type user_id: string
:param method_names: names of authentication methods
:type method_names: list
:param expires_at: optional time the token will expire
:type expires_at: string
:param project_id: optional project identity
:type project_id: string
:param domain_id: optional domain identity
:type domain_id: string
:param auth_context: optional context from the authorization plugins
:type auth_context: dict
:param trust: optional trust reference
:type trust: dict
:param include_catalog: optional, include the catalog in token data
:type include_catalog: boolean
:param parent_audit_id: optional, the audit id of the parent token
:type parent_audit_id: string
:returns: (token_id, token_data)
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def validate_token(self, token_ref):
"""Validate the given V3 token and return the token_data.
:param token_ref: the token reference
:type token_ref: dict
:returns: token data
:raises keystone.exception.TokenNotFound: If the token doesn't exist.
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def _get_token_id(self, token_data):
"""Generate the token_id based upon the data in token_data.
def generate_id_and_issued_at(self, token):
"""Generate a token based on the information provided.
:param token_data: token information
:type token_data: dict
:returns: token identifier
:rtype: six.text_type
:param token: A token object containing information about the
authorization context of the request.
:type token: `keystone.models.token.TokenModel`
:returns: tuple containing an ID for the token and the issued at time
of the token (token_id, issued_at).
"""
raise exception.NotImplemented() # pragma: no cover

View File

@ -466,6 +466,13 @@ class V3TokenDataHelper(provider_api.ProviderAPIMixin, object):
federated_info = token_data['user'].get('OS-FEDERATION')
if federated_info:
idp_id = federated_info['identity_provider']['id']
# FIXME(lbragstad): This isn't working properly because somewhere
# along the line we *were* encoding and decoding properly. This
# is needed to get some tests to pass in python 3. This will likely
# be fixed when the validate token path is moved over to using the
# token model, just like authenticate.
if isinstance(idp_id, bytes):
idp_id = idp_id.decode('utf-8')
PROVIDERS.federation_api.get_idp(idp_id)
def _populate_token_dates(self, token_data, expires=None, issued_at=None):
@ -553,52 +560,6 @@ class BaseProvider(provider_api.ProviderAPIMixin, base.Provider):
return (federation_constants.IDENTITY_PROVIDER in auth_context and
federation_constants.PROTOCOL in auth_context)
def issue_token(self, user_id, method_names, expires_at=None,
system=None, project_id=None, domain_id=None,
auth_context=None, trust=None, app_cred_id=None,
include_catalog=True, parent_audit_id=None):
if auth_context and auth_context.get('bind'):
# NOTE(lbragstad): Check if the token provider being used actually
# supports bind authentication methods before proceeding.
if not self._supports_bind_authentication:
raise exception.NotImplemented(_(
'The configured token provider does not support bind '
'authentication.'))
if trust:
if user_id != trust['trustee_user_id']:
raise exception.Forbidden(_('User is not a trustee.'))
token_ref = None
if auth_context and self._is_mapped_token(auth_context):
token_ref = self._handle_mapped_tokens(
auth_context, project_id, domain_id)
access_token = None
if 'oauth1' in method_names:
access_token_id = auth_context['access_token_id']
access_token = PROVIDERS.oauth_api.get_access_token(
access_token_id
)
token_data = self.v3_token_data_helper.get_token_data(
user_id,
method_names,
system=system,
domain_id=domain_id,
project_id=project_id,
expires=expires_at,
trust=trust,
app_cred_id=app_cred_id,
bind=auth_context.get('bind') if auth_context else None,
token=token_ref,
include_catalog=include_catalog,
access_token=access_token,
audit_info=parent_audit_id)
token_id = self._get_token_id(token_data)
return token_id, token_data
def _handle_mapped_tokens(self, auth_context, project_id, domain_id):
user_id = auth_context['user_id']
group_ids = auth_context['group_ids']
@ -630,59 +591,3 @@ class BaseProvider(provider_api.ProviderAPIMixin, base.Provider):
token_data, group_ids, project_id, domain_id, user_id)
return token_data
def validate_token(self, token_id):
try:
(user_id, methods, audit_ids, system, domain_id,
project_id, trust_id, federated_info, access_token_id,
app_cred_id, issued_at, expires_at) = (
self.token_formatter.validate_token(token_id))
except exception.ValidationError as e:
raise exception.TokenNotFound(e)
bind = None
token_dict = None
trust_ref = None
if federated_info:
# NOTE(lbragstad): We need to rebuild information about the
# federated token as well as the federated token roles. This is
# because when we validate a non-persistent token, we don't
# have a token reference to pull the federated token
# information out of. As a result, we have to extract it from
# the token itself and rebuild the federated context. These
# private methods currently live in the
# keystone.token.providers.fernet.Provider() class.
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 = PROVIDERS.trust_api.get_trust(trust_id)
access_token = None
if access_token_id:
access_token = PROVIDERS.oauth_api.get_access_token(
access_token_id
)
return self.v3_token_data_helper.get_token_data(
user_id,
method_names=methods,
system=system,
domain_id=domain_id,
project_id=project_id,
issued_at=issued_at,
expires=expires_at,
trust=trust_ref,
token=token_dict,
bind=bind,
access_token=access_token,
audit_info=audit_ids,
app_cred_id=app_cred_id)

View File

@ -15,16 +15,16 @@ import os
from keystone.common import utils as ks_utils
import keystone.conf
from keystone.federation import constants as federation_constants
from keystone import exception
from keystone.i18n import _
from keystone.token.providers import common
from keystone.token.providers import base
from keystone.token import token_formatters as tf
CONF = keystone.conf.CONF
class Provider(common.BaseProvider):
class Provider(base.Provider):
def __init__(self, *args, **kwargs):
super(Provider, self).__init__(*args, **kwargs)
@ -44,144 +44,33 @@ class Provider(common.BaseProvider):
self.token_formatter = tf.TokenFormatter()
def issue_token(self, *args, **kwargs):
token_id, token_data = super(Provider, self).issue_token(
*args, **kwargs)
self._build_issued_at_info(token_id, token_data)
return token_id, token_data
def _build_issued_at_info(self, token_id, token_data):
# NOTE(roxanaghe, lbragstad): We must use the creation time that
# Fernet builds into it's token. The Fernet spec details that the
# token creation time is built into the token, outside of the payload
# provided by Keystone. This is the reason why we don't pass the
# issued_at time in the payload. This also means that we shouldn't
# return a token reference with a creation time that we created
# when Fernet uses a different creation time. We should use the
# creation time provided by Fernet because it's the creation time
# that we have to rely on when we validate the token.
fernet_creation_datetime_obj = self.token_formatter.creation_time(
token_id)
if token_data.get('access'):
token_data['access']['token']['issued_at'] = ks_utils.isotime(
at=fernet_creation_datetime_obj, subsecond=True)
else:
token_data['token']['issued_at'] = ks_utils.isotime(
at=fernet_creation_datetime_obj, subsecond=True)
def _build_federated_info(self, token_data):
"""Extract everything needed for federated tokens.
This dictionary is passed to federated token formatters, which unpack
the values and build federated Fernet tokens.
"""
token_data = token_data['token']
try:
user = token_data['user']
federation = user[federation_constants.FEDERATION]
idp_id = federation['identity_provider']['id']
protocol_id = federation['protocol']['id']
except KeyError:
# The token data doesn't have federated info, so we aren't dealing
# with a federated token and no federated info to build.
return
group_ids = 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.
The federated_dict is passed back from the federated token formatters.
The responsibility of this method is to format the information passed
back from the token formatter into the token reference before
constructing the token data from the V3TokenDataHelper.
"""
g_ids = federated_dict['group_ids']
idp_id = federated_dict['idp_id']
protocol_id = federated_dict['protocol_id']
federated_info = {
'groups': g_ids,
'identity_provider': {'id': idp_id},
'protocol': {'id': protocol_id}
}
user_dict = self.identity_api.get_user(user_id)
user_name = user_dict['name']
token_dict = {
'user': {
federation_constants.FEDERATION: federated_info,
'id': user_id,
'name': user_name,
'domain': {'id': CONF.federation.federated_domain_name,
'name': CONF.federation.federated_domain_name, },
}
}
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_federated_user(
token_dict, group_ids, project_id, domain_id, user_id)
def _get_token_id(self, token_data):
"""Generate the token_id based upon the data in token_data.
:param token_data: token information
:type token_data: dict
:rtype: six.text_type
"""
user_id = token_data['token']['user']['id']
expires_at = token_data['token']['expires_at']
audit_ids = token_data['token']['audit_ids']
methods = token_data['token'].get('methods')
system = token_data['token'].get('system', {}).get('all', None)
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)
app_cred_id = token_data['token'].get('application_credential',
{}).get('id')
return self.token_formatter.create_token(
user_id,
expires_at,
audit_ids,
methods=methods,
system=system,
domain_id=domain_id,
project_id=project_id,
trust_id=trust_id,
federated_info=federated_info,
access_token_id=access_token_id,
app_cred_id=app_cred_id
def generate_id_and_issued_at(self, token):
token_id = self.token_formatter.create_token(
token.user_id,
token.expires_at,
token.audit_ids,
methods=token.methods,
system=token.system,
domain_id=token.domain_id,
project_id=token.project_id,
trust_id=token.trust_id,
federated_group_ids=token.federated_groups,
identity_provider_id=token.identity_provider_id,
protocol_id=token.protocol_id,
access_token_id=token.access_token_id,
app_cred_id=token.application_credential_id
)
creation_datetime_obj = self.token_formatter.creation_time(token_id)
issued_at = ks_utils.isotime(
at=creation_datetime_obj, subsecond=True
)
return token_id, issued_at
def validate_token(self, token_id):
try:
return self.token_formatter.validate_token(token_id)
except exception.ValidationError as e:
raise exception.TokenNotFound(e)
@property
def _supports_bind_authentication(self):

View File

@ -137,14 +137,17 @@ 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,
app_cred_id=None):
trust_id=None, federated_group_ids=None,
identity_provider_id=None, protocol_id=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,
federated_group_ids=federated_group_ids,
identity_provider_id=identity_provider_id,
protocol_id=protocol_id,
access_token_id=access_token_id,
app_cred_id=app_cred_id):
break
@ -152,7 +155,8 @@ class TokenFormatter(object):
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, app_cred_id
audit_ids, trust_id, federated_group_ids, identity_provider_id,
protocol_id, access_token_id, app_cred_id
)
versioned_payload = (version,) + payload
@ -185,8 +189,8 @@ class TokenFormatter(object):
for payload_class in PAYLOAD_CLASSES:
if version == payload_class.version:
(user_id, methods, system, project_id, domain_id,
expires_at, audit_ids, trust_id, federated_info,
access_token_id,
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id) = payload_class.disassemble(payload)
break
else:
@ -195,6 +199,13 @@ class TokenFormatter(object):
'This is not a recognized Fernet payload version: %s') %
version)
# FIXME(lbragstad): Without this, certain token validation tests fail
# when running with python 3. Once we get further along in this
# refactor, we should be better about handling string encoding/types at
# the edges of the application.
if isinstance(system, bytes):
system = system.decode('utf-8')
# rather than appearing in the payload, the creation time is encoded
# into the token format itself
issued_at = TokenFormatter.creation_time(token)
@ -203,8 +214,9 @@ 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, app_cred_id,
issued_at, expires_at)
trust_id, federated_group_ids, identity_provider_id,
protocol_id, access_token_id, app_cred_id, issued_at,
expires_at)
class BasePayload(object):
@ -224,8 +236,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id):
"""Assemble the payload of a token.
:param user_id: identifier of the user in the token request
@ -236,9 +249,9 @@ class BasePayload(object):
:param expires_at: datetime of the token's expiration
:param audit_ids: list of the token's audit IDs
:param trust_id: ID of the trust in effect
:param federated_info: dictionary containing group IDs, the identity
provider ID, protocol ID, and federated domain
ID
:param federated_group_ids: list of group IDs from SAML assertion
:param identity_provider_id: ID of the user's identity provider
:param protocol_id: federated protocol used for authentication
: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
@ -253,12 +266,10 @@ class BasePayload(object):
The tuple consists of::
(user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_info,
access_token_id, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id,` access_token_id, app_cred_id)
* ``methods`` are the auth methods.
* federated_info is a dict contains the group IDs, the identity
provider ID, the protocol ID, and the federated domain ID
Fields will be set to None if they didn't apply to this payload type.
@ -365,8 +376,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -386,12 +398,15 @@ class UnscopedPayload(BasePayload):
project_id = None
domain_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class DomainScopedPayload(BasePayload):
@ -403,8 +418,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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:
@ -441,12 +457,15 @@ class DomainScopedPayload(BasePayload):
system = None
project_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class ProjectScopedPayload(BasePayload):
@ -458,8 +477,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -482,12 +502,15 @@ class ProjectScopedPayload(BasePayload):
system = None
domain_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class TrustScopedPayload(BasePayload):
@ -499,8 +522,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -526,12 +550,15 @@ class TrustScopedPayload(BasePayload):
trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
system = None
domain_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class FederatedUnscopedPayload(BasePayload):
@ -539,7 +566,7 @@ class FederatedUnscopedPayload(BasePayload):
@classmethod
def create_arguments_apply(cls, **kwargs):
return kwargs['federated_info']
return kwargs['federated_group_ids']
@classmethod
def pack_group_id(cls, group_dict):
@ -554,15 +581,13 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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,
federated_info['group_ids']))
b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
federated_info['idp_id'])
protocol_id = federated_info['protocol_id']
b_group_ids = list(map(cls.pack_group_id, federated_group_ids))
b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(identity_provider_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))
@ -587,8 +612,6 @@ class FederatedUnscopedPayload(BasePayload):
protocol_id = protocol_id.decode('utf-8')
expires_at_str = cls._convert_float_to_time_string(payload[5])
audit_ids = list(map(cls.base64_encode, payload[6]))
federated_info = dict(group_ids=group_ids, idp_id=idp_id,
protocol_id=protocol_id)
system = None
project_id = None
domain_id = None
@ -596,8 +619,8 @@ class FederatedUnscopedPayload(BasePayload):
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, app_cred_id)
expires_at_str, audit_ids, trust_id, group_ids, idp_id,
protocol_id, access_token_id, app_cred_id)
class FederatedScopedPayload(FederatedUnscopedPayload):
@ -605,17 +628,15 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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(
project_id or domain_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']
b_group_ids = list(map(cls.pack_group_id, federated_group_ids))
b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(identity_provider_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))
@ -645,15 +666,13 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
protocol_id = payload[5]
expires_at_str = cls._convert_float_to_time_string(payload[6])
audit_ids = list(map(cls.base64_encode, payload[7]))
federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
group_ids=group_ids)
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, app_cred_id)
expires_at_str, audit_ids, trust_id, group_ids, idp_id,
protocol_id, access_token_id, app_cred_id)
class FederatedProjectScopedPayload(FederatedScopedPayload):
@ -661,7 +680,7 @@ class FederatedProjectScopedPayload(FederatedScopedPayload):
@classmethod
def create_arguments_apply(cls, **kwargs):
return kwargs['project_id'] and kwargs['federated_info']
return kwargs['project_id'] and kwargs['federated_group_ids']
class FederatedDomainScopedPayload(FederatedScopedPayload):
@ -669,7 +688,7 @@ class FederatedDomainScopedPayload(FederatedScopedPayload):
@classmethod
def create_arguments_apply(cls, **kwargs):
return kwargs['domain_id'] and kwargs['federated_info']
return kwargs['domain_id'] and kwargs['federated_group_ids']
class OauthScopedPayload(BasePayload):
@ -681,8 +700,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -711,12 +731,15 @@ class OauthScopedPayload(BasePayload):
system = None
domain_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class SystemScopedPayload(BasePayload):
@ -728,8 +751,9 @@ 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, app_cred_id):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -749,12 +773,15 @@ class SystemScopedPayload(BasePayload):
project_id = None
domain_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_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, app_cred_id)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
class ApplicationCredentialScopedPayload(BasePayload):
@ -766,8 +793,9 @@ class ApplicationCredentialScopedPayload(BasePayload):
@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):
expires_at, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_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)
@ -792,14 +820,17 @@ class ApplicationCredentialScopedPayload(BasePayload):
system = None
domain_id = None
trust_id = None
federated_info = None
federated_group_ids = None
identity_provider_id = None
protocol_id = 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)
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
app_cred_id)
# For now, the order of the classes in the following list is important. This

View File

@ -0,0 +1,18 @@
---
upgrade:
- |
[`bug 1778945 <https://bugs.launchpad.net/keystone/+bug/1778945>`_]
The pluggable interface for token providers has changed. If you're
maintaining a custom token provider, you're going to be affected by these
interface changes. Implementing the new interface will be required before
using your custom token provider with the Rocky release of keystone. The
new interface is more clear about the relationship and responsibilities
between the token API and pluggable token providers.
fixes:
- |
[`bug 1778945 <https://bugs.launchpad.net/keystone/+bug/1778945>`_]
There were several improvements made to the token provider API and
interface that simplify what external developers need to do and understand
in order to provide their own token provider implementation. Please see the
linked bug report for more details as to why these changes were made and
the benefits they provide for both upstream and downstream developers.