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:
parent
693a86f2a1
commit
b47e84dac1
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue