summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-07-15 17:03:50 +0000
committerGerrit Code Review <review@openstack.org>2013-07-15 17:03:50 +0000
commitdec66cd5a716653d25ea8a79827695424d15862d (patch)
tree7a872919f5076ece1472b18982e4f17f28545c32
parentdf63b9c71f2aa5ba727d10f00d4a4b3538d6fcff (diff)
parentc238ace30981877e5991874c5b193ea7d5107419 (diff)
Merge "Implements Pluggable V3 Token Provider"
-rw-r--r--doc/source/configuration.rst26
-rw-r--r--etc/keystone.conf.sample4
-rw-r--r--keystone/auth/controllers.py85
-rw-r--r--keystone/auth/plugins/password.py12
-rw-r--r--keystone/auth/plugins/token.py3
-rw-r--r--keystone/auth/token_factory.py368
-rw-r--r--keystone/common/config.py6
-rw-r--r--keystone/common/sql/core.py7
-rw-r--r--keystone/service.py3
-rw-r--r--keystone/test.py4
-rw-r--r--keystone/token/__init__.py1
-rw-r--r--keystone/token/backends/kvs.py3
-rw-r--r--keystone/token/backends/memcache.py12
-rw-r--r--keystone/token/backends/sql.py6
-rw-r--r--keystone/token/core.py37
-rw-r--r--keystone/token/provider.py136
-rw-r--r--keystone/token/providers/__init__.py0
-rw-r--r--keystone/token/providers/pki.py44
-rw-r--r--keystone/token/providers/uuid.py349
-rw-r--r--tests/test_auth.py14
-rw-r--r--tests/test_auth_plugin.py6
-rw-r--r--tests/test_backend_memcache.py2
-rw-r--r--tests/test_cert_setup.py1
-rw-r--r--tests/test_pki_token_provider.conf5
-rw-r--r--tests/test_token_provider.py396
-rw-r--r--tests/test_uuid_token_provider.conf5
-rw-r--r--tests/test_v3.py18
-rw-r--r--tests/test_v3_auth.py147
28 files changed, 1143 insertions, 557 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 03fa1d63..daa0896f 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -74,7 +74,7 @@ following sections:
* ``[s3]`` - Amazon S3 authentication driver configuration.
* ``[identity]`` - identity system driver configuration
* ``[catalog]`` - service catalog driver configuration
-* ``[token]`` - token driver configuration
+* ``[token]`` - token driver & token provider configuration
* ``[policy]`` - policy system driver configuration for RBAC
* ``[signing]`` - cryptographic signatures for PKI based tokens
* ``[ssl]`` - SSL configuration
@@ -148,6 +148,26 @@ invoked, all plugins must succeed in order to for the entire
authentication to be successful. Furthermore, all the plugins invoked must
agree on the ``user_id`` in the ``auth_context``.
+Token Provider
+--------------
+
+Keystone supports customizable token provider and it is specified in the
+``[token]`` section of the configuration file. Keystone provides both UUID and
+PKI token providers, with PKI token provider enabled as default. However, users
+may register their own token provider by configuring the following property.
+
+* ``provider`` - token provider driver. Defaults to
+ ``keystone.token.providers.pki.Provider``
+
+Note that ``token_format`` in the ``[signing]`` section is deprecated but still
+being supported for backward compatibility. Therefore, if ``provider`` is set
+to ``keystone.token.providers.pki.Provider``, ``token_format`` must be ``PKI``.
+Conversely, if ``provider`` is ``keystone.token.providers.uuid.Provider``,
+``token_format`` must be ``UUID``.
+
+For a customized provider, ``token_format`` must not set to ``PKI`` or
+``UUID``.
+
Certificates for PKI
--------------------
@@ -163,7 +183,9 @@ private key should only be readable by the system user that will run Keystone.
The values that specify where to read the certificates are under the
``[signing]`` section of the configuration file. The configuration values are:
-* ``token_format`` - Determines the algorithm used to generate tokens. Can be either ``UUID`` or ``PKI``. Defaults to ``PKI``
+* ``token_format`` - Determines the algorithm used to generate tokens. Can be
+ either ``UUID`` or ``PKI``. Defaults to ``PKI``. This option must be used in
+ conjunction with ``provider`` configuration in the ``[token]`` section.
* ``certfile`` - Location of certificate used to verify tokens. Default is ``/etc/keystone/ssl/certs/signing_cert.pem``
* ``keyfile`` - Location of private key used to sign tokens. Default is ``/etc/keystone/ssl/private/signing_key.pem``
* ``ca_certs`` - Location of certificate for the authority that issued the above certificate. Default is ``/etc/keystone/ssl/certs/ca.pem``
diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample
index 3f4f1637..7ab9acdc 100644
--- a/etc/keystone.conf.sample
+++ b/etc/keystone.conf.sample
@@ -119,8 +119,12 @@
# template_file = default_catalog.templates
[token]
+# Provides token persistence.
# driver = keystone.token.backends.sql.Token
+# Controls the token construction, validation, and revocation operations.
+# provider = keystone.token.providers.pki.Provider
+
# Amount of time a token should remain valid (in seconds)
# expiration = 86400
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py
index bef4128d..47c44b03 100644
--- a/keystone/auth/controllers.py
+++ b/keystone/auth/controllers.py
@@ -14,12 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
-from keystone.auth import token_factory
-from keystone.common import cms
from keystone.common import controller
+from keystone.common import dependency
from keystone.common import logging
+from keystone.common import wsgi
from keystone import config
from keystone import exception
from keystone import identity
@@ -190,6 +189,10 @@ class AuthInfo(object):
self._scope_data = (None, None, trust_ref)
def _validate_auth_methods(self):
+ if 'identity' not in self.auth:
+ raise exception.ValidationError(attribute='identity',
+ target='auth')
+
# make sure auth methods are provided
if 'methods' not in self.auth['identity']:
raise exception.ValidationError(attribute='methods',
@@ -267,6 +270,7 @@ class AuthInfo(object):
self._scope_data = (domain_id, project_id, trust)
+@dependency.requires('token_provider_api')
class Auth(controller.V3Controller):
def __init__(self, *args, **kw):
super(Auth, self).__init__(*args, **kw)
@@ -280,14 +284,22 @@ class Auth(controller.V3Controller):
auth_context = {'extras': {}, 'method_names': []}
self.authenticate(context, auth_info, auth_context)
self._check_and_set_default_scoping(auth_info, auth_context)
- (token_id, token_data) = token_factory.create_token(
- auth_context, auth_info)
- return token_factory.render_token_data_response(
- token_id, token_data, created=True)
- except exception.SecurityError:
- raise
- except Exception as e:
- LOG.exception(e)
+ (domain_id, project_id, trust) = auth_info.get_scope()
+ method_names = auth_info.get_method_names()
+ method_names += auth_context.get('method_names', [])
+ # make sure the list is unique
+ method_names = list(set(method_names))
+ (token_id, token_data) = self.token_provider_api.issue_token(
+ user_id=auth_context['user_id'],
+ method_names=method_names,
+ expires_at=auth_context.get('expires_at'),
+ project_id=project_id,
+ domain_id=domain_id,
+ auth_context=auth_context,
+ trust=trust)
+ return render_token_data_response(token_id, token_data,
+ created=True)
+ except exception.TrustNotFound as e:
raise exception.Unauthorized(e)
def _check_and_set_default_scoping(self, auth_info, auth_context):
@@ -355,44 +367,41 @@ class Auth(controller.V3Controller):
msg = _('User not found')
raise exception.Unauthorized(msg)
- def _get_token_ref(self, context, token_id, belongs_to=None):
- token_ref = self.token_api.get_token(token_id)
- if cms.is_ans1_token(token_id):
- verified_token = cms.cms_verify(cms.token_to_cms(token_id),
- CONF.signing.certfile,
- CONF.signing.ca_certs)
- token_ref = json.loads(verified_token)
- if belongs_to:
- assert token_ref['project']['id'] == belongs_to
- return token_ref
-
@controller.protected
def check_token(self, context):
- try:
- token_id = context.get('subject_token_id')
- belongs_to = context['query_string'].get('belongsTo')
- assert self._get_token_ref(context, token_id, belongs_to)
- except Exception as e:
- LOG.error(e)
- raise exception.Unauthorized(e)
+ token_id = context.get('subject_token_id')
+ self.token_provider_api.check_token(token_id)
@controller.protected
def revoke_token(self, context):
token_id = context.get('subject_token_id')
- return self.token_controllers_ref.delete_token(context, token_id)
+ return self.token_provider_api.revoke_token(token_id)
@controller.protected
def validate_token(self, context):
token_id = context.get('subject_token_id')
- self.check_token(context)
- token_ref = self.token_api.get_token(token_id)
- token_data = token_factory.recreate_token_data(
- token_ref.get('token_data'),
- token_ref['expires'],
- token_ref.get('user'),
- token_ref.get('tenant'))
- return token_factory.render_token_data_response(token_id, token_data)
+ token_data = self.token_provider_api.validate_token(token_id)
+ return render_token_data_response(token_id, token_data)
@controller.protected
def revocation_list(self, context, auth=None):
return self.token_controllers_ref.revocation_list(context, auth)
+
+
+#FIXME(gyee): not sure if it belongs here or keystone.common. Park it here
+# for now.
+def render_token_data_response(token_id, token_data, created=False):
+ """Render token data HTTP response.
+
+ Stash token ID into the X-Subject-Token header.
+
+ """
+ headers = [('X-Subject-Token', token_id)]
+
+ if created:
+ status = (201, 'Created')
+ else:
+ status = (200, 'OK')
+
+ return wsgi.render_response(body=token_data,
+ status=status, headers=headers)
diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py
index 631ce08d..f3cfeba8 100644
--- a/keystone/auth/plugins/password.py
+++ b/keystone/auth/plugins/password.py
@@ -103,8 +103,14 @@ class Password(auth.AuthMethodHandler):
# FIXME(gyee): identity.authenticate() can use some refactoring since
# all we care is password matches
- self.identity_api.authenticate(
- user_id=user_info.user_id,
- password=user_info.password)
+ try:
+ self.identity_api.authenticate(
+ user_id=user_info.user_id,
+ password=user_info.password)
+ except AssertionError:
+ # authentication failed because of invalid username or password
+ msg = _('Invalid username or password')
+ raise exception.Unauthorized(msg)
+
if 'user_id' not in user_context:
user_context['user_id'] = user_info.user_id
diff --git a/keystone/auth/plugins/token.py b/keystone/auth/plugins/token.py
index d9b3d2f8..e9982733 100644
--- a/keystone/auth/plugins/token.py
+++ b/keystone/auth/plugins/token.py
@@ -46,7 +46,8 @@ class Token(auth.AuthMethodHandler):
token_ref['token_data']['token']['extras'])
user_context['method_names'].extend(
token_ref['token_data']['token']['methods'])
- if 'trust' in token_ref['token_data']:
+ if ('OS-TRUST:trust' in token_ref['token_data']['token'] or
+ 'trust' in token_ref['token_data']['token']):
raise exception.Forbidden()
except AssertionError as e:
LOG.error(e)
diff --git a/keystone/auth/token_factory.py b/keystone/auth/token_factory.py
deleted file mode 100644
index 22bc8363..00000000
--- a/keystone/auth/token_factory.py
+++ /dev/null
@@ -1,368 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2013 OpenStack LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""Token Factory"""
-
-import json
-import sys
-import uuid
-import webob
-
-from keystone import catalog
-from keystone.common import cms
-from keystone.common import environment
-from keystone.common import logging
-from keystone.common import utils
-from keystone import config
-from keystone import exception
-from keystone import identity
-from keystone.openstack.common import jsonutils
-from keystone.openstack.common import timeutils
-from keystone import token as token_module
-from keystone import trust
-
-
-CONF = config.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class TokenDataHelper(object):
- """Token data helper."""
- def __init__(self):
- self.identity_api = identity.Manager()
- self.catalog_api = catalog.Manager()
- self.trust_api = trust.Manager()
-
- def _get_filtered_domain(self, domain_id):
- domain_ref = self.identity_api.get_domain(domain_id)
- return {'id': domain_ref['id'], 'name': domain_ref['name']}
-
- def _populate_scope(self, token_data, domain_id, project_id):
- if 'domain' in token_data or 'project' in token_data:
- return
-
- if domain_id:
- token_data['domain'] = self._get_filtered_domain(domain_id)
- if project_id:
- project_ref = self.identity_api.get_project(project_id)
- filtered_project = {
- 'id': project_ref['id'],
- 'name': project_ref['name']}
- filtered_project['domain'] = self._get_filtered_domain(
- project_ref['domain_id'])
- token_data['project'] = filtered_project
-
- def _get_project_roles_for_user(self, user_id, project_id):
- roles = self.identity_api.get_roles_for_user_and_project(
- user_id, project_id)
- roles_ref = []
- for role_id in roles:
- role_ref = self.identity_api.get_role(role_id)
- role_ref.setdefault('project_id', project_id)
- roles_ref.append(role_ref)
- # user have no project roles, therefore access denied
- if len(roles_ref) == 0:
- msg = _('User have no access to project')
- LOG.debug(msg)
- raise exception.Unauthorized(msg)
- return roles_ref
-
- def _get_domain_roles_for_user(self, user_id, domain_id):
- roles = self.identity_api.get_roles_for_user_and_domain(
- user_id, domain_id)
- roles_ref = []
- for role_id in roles:
- role_ref = self.identity_api.get_role(role_id)
- role_ref.setdefault('domain_id', domain_id)
- roles_ref.append(role_ref)
- # user have no domain roles, therefore access denied
- if len(roles_ref) == 0:
- msg = _('User have no access to domain')
- LOG.debug(msg)
- raise exception.Unauthorized(msg)
- return roles_ref
-
- def _get_roles_for_user(self, user_id, domain_id, project_id):
- roles = []
- if domain_id:
- roles = self._get_domain_roles_for_user(user_id, domain_id)
- if project_id:
- roles = self._get_project_roles_for_user(user_id, project_id)
- return roles
-
- def _populate_user(self, token_data, user_id, domain_id, project_id,
- trust):
- if 'user' in token_data:
- return
-
- user_ref = self.identity_api.get_user(user_id)
- if CONF.trust.enabled and trust:
- trustor_user_ref = self.identity_api.get_user(
- trust['trustor_user_id'])
- if not trustor_user_ref['enabled']:
- raise exception.Forbidden()
- if trust['impersonation']:
- user_ref = trustor_user_ref
- token_data['OS-TRUST:trust'] = (
- {
- 'id': trust['id'],
- 'trustor_user': {'id': trust['trustor_user_id']},
- 'trustee_user': {'id': trust['trustee_user_id']},
- 'impersonation': trust['impersonation']
- })
- filtered_user = {
- 'id': user_ref['id'],
- 'name': user_ref['name'],
- 'domain': self._get_filtered_domain(user_ref['domain_id'])}
- token_data['user'] = filtered_user
-
- def _populate_roles(self, token_data, user_id, domain_id, project_id,
- trust):
- if 'roles' in token_data:
- return
-
- if CONF.trust.enabled and trust:
- token_user_id = trust['trustor_user_id']
- token_project_id = trust['project_id']
- #trusts do not support domains yet
- token_domain_id = None
- else:
- token_user_id = user_id
- token_project_id = project_id
- token_domain_id = domain_id
-
- if token_domain_id or token_project_id:
- roles = self._get_roles_for_user(token_user_id,
- token_domain_id,
- token_project_id)
- filtered_roles = []
- if CONF.trust.enabled and trust:
- for trust_role in trust['roles']:
- match_roles = [x for x in roles
- if x['id'] == trust_role['id']]
- if match_roles:
- filtered_roles.append(match_roles[0])
- else:
- raise exception.Forbidden()
- else:
- for role in roles:
- filtered_roles.append({'id': role['id'],
- 'name': role['name']})
- token_data['roles'] = filtered_roles
-
- def _populate_service_catalog(self, token_data, user_id,
- domain_id, project_id, trust):
- if 'catalog' in token_data:
- return
-
- if CONF.trust.enabled and trust:
- user_id = trust['trustor_user_id']
- if project_id or domain_id:
- try:
- service_catalog = self.catalog_api.get_v3_catalog(
- user_id, project_id)
- # TODO(ayoung): KVS backend needs a sample implementation
- except exception.NotImplemented:
- service_catalog = {}
- # TODO(gyee): v3 service catalog is not quite completed yet
- # TODO(ayoung): Enforce Endpoints for trust
- token_data['catalog'] = service_catalog
-
- def _populate_token(self, token_data, expires=None, trust=None):
- if not expires:
- expires = token_module.default_expire_time()
- if not isinstance(expires, basestring):
- expires = timeutils.isotime(expires, subsecond=True)
- token_data['expires_at'] = expires
- token_data['issued_at'] = timeutils.isotime(subsecond=True)
-
- def get_token_data(self, user_id, method_names, extras,
- domain_id=None, project_id=None, expires=None,
- trust=None, token=None):
- token_data = {'methods': method_names,
- 'extras': extras}
-
- # We've probably already written these to the token
- for x in ('roles', 'user', 'catalog', 'project', 'domain'):
- if token and x in token:
- token_data[x] = token[x]
-
- if CONF.trust.enabled and trust:
- if user_id != trust['trustee_user_id']:
- raise exception.Forbidden()
-
- self._populate_scope(token_data, domain_id, project_id)
- self._populate_user(token_data, user_id, domain_id, project_id, trust)
- self._populate_roles(token_data, user_id, domain_id, project_id, trust)
- self._populate_service_catalog(token_data, user_id, domain_id,
- project_id, trust)
- self._populate_token(token_data, expires, trust)
- return {'token': token_data}
-
-
-def recreate_token_data(token_data=None, expires=None,
- user_ref=None, project_ref=None):
- """Recreate token from an existing token.
-
- Repopulate the ephemeral data and return the new token data.
-
- """
- new_expires = expires
- project_id = None
- user_id = None
- domain_id = None
- methods = ['password', 'token']
- extras = {}
-
- # NOTE(termie): Let's get some things straight here, because this code
- # is wrong but tested as such:
- # token_data, if it exists, is going to look like:
- # {'token': ... the actual token data + a superfluous extras field ...}
- # this data is actually stored in the database in the 'extras' column and
- # then deserialized and added to the token_ref, that already has the
- # the 'expires', 'user_id', and 'id' columns from the db.
- # the 'user' and 'tenant' fields are being added to the
- # token_ref due to being deserialized from the 'extras' column
- #
- # So, how this all looks in the db:
- # id = some_id
- # user_id = some_user_id
- # expires = some_expiration
- # extras = {'user': {'id': some_used_id},
- # 'tenant': {'id': some_tenant_id},
- # 'token_data': 'token': {'domain': {'id': some_domain_id},
- # 'project': {'id': some_project_id},
- # 'domain': {'id': some_domain_id},
- # 'user': {'id': some_user_id},
- # 'roles': [{'id': some_role_id}, ...],
- # 'catalog': ...,
- # 'expires_at': some_expiry_time,
- # 'issued_at': now(),
- # 'methods': ['password', 'token'],
- # 'extras': { ... empty? ...}
- #
- # TODO(termie): reduce stored token complexity, bug filed at:
- # https://bugs.launchpad.net/keystone/+bug/1159990
- if token_data:
- # peel the outer layer so its easier to operate
- token = token_data['token']
- domain_id = (token['domain']['id'] if 'domain' in token
- else None)
- project_id = (token['project']['id'] if 'project' in token
- else None)
- if not new_expires:
- # support Grizzly-3 to Grizzly-RC1 transition
- # tokens issued in G3 has 'expires' instead of 'expires_at'
- new_expires = token.get('expires_at',
- token.get('expires'))
- user_id = token['user']['id']
- methods = token['methods']
- extras = token['extras']
- else:
- token = None
- project_id = project_ref['id'] if project_ref else None
- user_id = user_ref['id']
- token_data_helper = TokenDataHelper()
- return token_data_helper.get_token_data(user_id,
- methods,
- extras,
- domain_id,
- project_id,
- new_expires,
- token=token)
-
-
-def create_token(auth_context, auth_info):
- token_data_helper = TokenDataHelper()
- (domain_id, project_id, trust) = auth_info.get_scope()
- method_names = list(set(auth_info.get_method_names() +
- auth_context.get('method_names', [])))
- token_data = token_data_helper.get_token_data(
- auth_context['user_id'],
- method_names,
- auth_context['extras'],
- domain_id,
- project_id,
- auth_context.get('expires_at', None),
- trust)
-
- if CONF.signing.token_format == 'UUID':
- token_id = uuid.uuid4().hex
- elif CONF.signing.token_format == 'PKI':
- try:
- token_id = cms.cms_sign_token(json.dumps(token_data),
- CONF.signing.certfile,
- CONF.signing.keyfile)
- except environment.subprocess.CalledProcessError:
- raise exception.UnexpectedError(_(
- 'Unable to sign token.'))
- else:
- raise exception.UnexpectedError(_(
- 'Invalid value for token_format: %s.'
- ' Allowed values are PKI or UUID.') %
- CONF.signing.token_format)
- token_api = token_module.Manager()
- try:
- expiry = token_data['token']['expires_at']
- if isinstance(expiry, basestring):
- expiry = timeutils.normalize_time(timeutils.parse_isotime(expiry))
- role_ids = []
- if 'project' in token_data['token']:
- # project-scoped token, fill in the v2 token data
- # all we care are the role IDs
- role_ids = [role['id'] for role in token_data['token']['roles']]
- metadata_ref = {'roles': role_ids}
- data = dict(key=token_id,
- id=token_id,
- expires=expiry,
- user=token_data['token']['user'],
- tenant=token_data['token'].get('project'),
- metadata=metadata_ref,
- token_data=token_data,
- trust_id=trust['id'] if trust else None)
- token_api.create_token(token_id, data)
- except Exception:
- exc_info = sys.exc_info()
- # an identical token may have been created already.
- # if so, return the token_data as it is also identical
- try:
- token_api.get_token(token_id)
- except exception.TokenNotFound:
- raise exc_info[0], exc_info[1], exc_info[2]
-
- return (token_id, token_data)
-
-
-def render_token_data_response(token_id, token_data, created=False):
- """Render token data HTTP response.
-
- Stash token ID into the X-Auth-Token header.
-
- """
- headers = [('X-Subject-Token', token_id)]
- headers.append(('Vary', 'X-Auth-Token'))
- headers.append(('Content-Type', 'application/json'))
-
- if created:
- status = (201, 'Created')
- else:
- status = (200, 'OK')
-
- body = jsonutils.dumps(token_data, cls=utils.SmarterEncoder)
- return webob.Response(body=body,
- status='%s %s' % status,
- headerlist=headers)
diff --git a/keystone/common/config.py b/keystone/common/config.py
index c54b57f6..905f67b7 100644
--- a/keystone/common/config.py
+++ b/keystone/common/config.py
@@ -399,3 +399,9 @@ def configure():
# PasteDeploy config file
register_str('config_file', group='paste_deploy', default=None)
+
+ # token provider
+ register_str(
+ 'provider',
+ group='token',
+ default='keystone.token.providers.pki.Provider')
diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py
index 9e79ab36..7978fcc5 100644
--- a/keystone/common/sql/core.py
+++ b/keystone/common/sql/core.py
@@ -80,13 +80,6 @@ def initialize_decorator(init):
v = str(v)
if column.type.length and \
column.type.length < len(v):
- #if signing.token_format == 'PKI', the id will
- #store it's public key which is very long.
- if config.CONF.signing.token_format == 'PKI' and \
- self.__tablename__ == 'token' and \
- k == 'id':
- continue
-
raise exception.StringLengthExceeded(
string=v, type=k, length=column.type.length)
diff --git a/keystone/service.py b/keystone/service.py
index 406004d6..6b0c3708 100644
--- a/keystone/service.py
+++ b/keystone/service.py
@@ -41,7 +41,8 @@ DRIVERS = dict(
identity_api=identity.Manager(),
policy_api=policy.Manager(),
token_api=token.Manager(),
- trust_api=trust.Manager())
+ trust_api=trust.Manager(),
+ token_provider_api=token.provider.Manager())
@logging.fail_gracefully
diff --git a/keystone/test.py b/keystone/test.py
index 5977537e..0c51d76d 100644
--- a/keystone/test.py
+++ b/keystone/test.py
@@ -252,8 +252,8 @@ class TestCase(NoModule, unittest.TestCase):
def load_backends(self):
"""Initializes each manager and assigns them to an attribute."""
- for manager in [assignment, catalog, credential,
- identity, policy, token, trust]:
+ for manager in [assignment, catalog, credential, identity, policy,
+ token, trust]:
manager_name = '%s_api' % manager.__name__.split('.')[-1]
setattr(self, manager_name, manager.Manager())
diff --git a/keystone/token/__init__.py b/keystone/token/__init__.py
index 889cd39a..ffd9bc44 100644
--- a/keystone/token/__init__.py
+++ b/keystone/token/__init__.py
@@ -17,4 +17,5 @@
from keystone.token import controllers
from keystone.token.core import *
+from keystone.token import provider
from keystone.token import routers
diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py
index c16dd61b..0927aba1 100644
--- a/keystone/token/backends/kvs.py
+++ b/keystone/token/backends/kvs.py
@@ -26,7 +26,6 @@ class Token(kvs.Base, token.Driver):
# Public interface
def get_token(self, token_id):
- token_id = token.unique_id(token_id)
try:
ref = self.db.get('token-%s' % token_id)
except exception.NotFound:
@@ -41,7 +40,6 @@ class Token(kvs.Base, token.Driver):
raise exception.TokenNotFound(token_id=token_id)
def create_token(self, token_id, data):
- token_id = token.unique_id(token_id)
data_copy = copy.deepcopy(data)
data_copy['id'] = token_id
if not data_copy.get('expires'):
@@ -52,7 +50,6 @@ class Token(kvs.Base, token.Driver):
return copy.deepcopy(data_copy)
def delete_token(self, token_id):
- token_id = token.unique_id(token_id)
try:
token_ref = self.get_token(token_id)
self.db.delete('token-%s' % token_id)
diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py
index e9d8482f..06e89d60 100644
--- a/keystone/token/backends/memcache.py
+++ b/keystone/token/backends/memcache.py
@@ -65,7 +65,7 @@ class Token(token.Driver):
def get_token(self, token_id):
if token_id is None:
raise exception.TokenNotFound(token_id='')
- ptk = self._prefix_token_id(token.unique_id(token_id))
+ ptk = self._prefix_token_id(token_id)
token_ref = self.client.get(ptk)
if token_ref is None:
raise exception.TokenNotFound(token_id=token_id)
@@ -74,7 +74,7 @@ class Token(token.Driver):
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
- ptk = self._prefix_token_id(token.unique_id(token_id))
+ ptk = self._prefix_token_id(token_id)
if not data_copy.get('expires'):
data_copy['expires'] = token.default_expire_time()
if not data_copy.get('user_id'):
@@ -118,7 +118,7 @@ class Token(token.Driver):
if record is not None:
token_list = jsonutils.loads('[%s]' % record)
for token_i in token_list:
- ptk = self._prefix_token_id(token.unique_id(token_i))
+ ptk = self._prefix_token_id(token_i)
token_ref = self.client.get(ptk)
if not token_ref:
# skip tokens that do not exist in memcache
@@ -174,8 +174,8 @@ class Token(token.Driver):
def delete_token(self, token_id):
# Test for existence
- data = self.get_token(token.unique_id(token_id))
- ptk = self._prefix_token_id(token.unique_id(token_id))
+ data = self.get_token(token_id)
+ ptk = self._prefix_token_id(token_id)
result = self.client.delete(ptk)
self._add_to_revocation_list(data)
return result
@@ -186,7 +186,7 @@ class Token(token.Driver):
user_record = self.client.get(user_key) or ""
token_list = jsonutils.loads('[%s]' % user_record)
for token_id in token_list:
- ptk = self._prefix_token_id(token.unique_id(token_id))
+ ptk = self._prefix_token_id(token_id)
token_ref = self.client.get(ptk)
if token_ref:
if tenant_id is not None:
diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py
index 57dbf410..0e8a916d 100644
--- a/keystone/token/backends/sql.py
+++ b/keystone/token/backends/sql.py
@@ -41,7 +41,7 @@ class Token(sql.Base, token.Driver):
if token_id is None:
raise exception.TokenNotFound(token_id=token_id)
session = self.get_session()
- token_ref = session.query(TokenModel).get(token.unique_id(token_id))
+ token_ref = session.query(TokenModel).get(token_id)
now = datetime.datetime.utcnow()
if not token_ref or not token_ref.valid:
raise exception.TokenNotFound(token_id=token_id)
@@ -59,7 +59,6 @@ class Token(sql.Base, token.Driver):
data_copy['user_id'] = data_copy['user']['id']
token_ref = TokenModel.from_dict(data_copy)
- token_ref.id = token.unique_id(token_id)
token_ref.valid = True
session = self.get_session()
with session.begin():
@@ -69,9 +68,8 @@ class Token(sql.Base, token.Driver):
def delete_token(self, token_id):
session = self.get_session()
- key = token.unique_id(token_id)
with session.begin():
- token_ref = session.query(TokenModel).get(key)
+ token_ref = session.query(TokenModel).get(token_id)
if not token_ref or not token_ref.valid:
raise exception.TokenNotFound(token_id=token_id)
token_ref.valid = False
diff --git a/keystone/token/core.py b/keystone/token/core.py
index a8a3b82d..bc27b80d 100644
--- a/keystone/token/core.py
+++ b/keystone/token/core.py
@@ -16,6 +16,7 @@
"""Main entry point into the Token service."""
+import copy
import datetime
from keystone.common import cms
@@ -32,19 +33,6 @@ config.register_int('expiration', group='token', default=86400)
LOG = logging.getLogger(__name__)
-def unique_id(token_id):
- """Return a unique ID for a token.
-
- The returned value is useful as the primary key of a database table,
- memcache store, or other lookup table.
-
- :returns: Given a PKI token, returns it's hashed value. Otherwise, returns
- the passed-in value (such as a UUID token ID or an existing
- hash).
- """
- return cms.cms_hash_token(token_id)
-
-
def default_expire_time():
"""Determine when a fresh token should expire.
@@ -114,6 +102,29 @@ class Manager(manager.Manager):
def __init__(self):
super(Manager, self).__init__(CONF.token.driver)
+ def _unique_id(self, token_id):
+ """Return a unique ID for a token.
+
+ The returned value is useful as the primary key of a database table,
+ memcache store, or other lookup table.
+
+ :returns: Given a PKI token, returns it's hashed value. Otherwise,
+ returns the passed-in value (such as a UUID token ID or an
+ existing hash).
+ """
+ return cms.cms_hash_token(token_id)
+
+ def get_token(self, token_id):
+ return self.driver.get_token(self._unique_id(token_id))
+
+ def create_token(self, token_id, data):
+ data_copy = copy.deepcopy(data)
+ data_copy['id'] = self._unique_id(token_id)
+ return self.driver.create_token(self._unique_id(token_id), data_copy)
+
+ def delete_token(self, token_id):
+ return self.driver.delete_token(self._unique_id(token_id))
+
class Driver(object):
"""Interface description for a Token driver."""
diff --git a/keystone/token/provider.py b/keystone/token/provider.py
new file mode 100644
index 00000000..b5ad72fa
--- /dev/null
+++ b/keystone/token/provider.py
@@ -0,0 +1,136 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Token provider interface."""
+
+
+from keystone.common import dependency
+from keystone.common import logging
+from keystone.common import manager
+from keystone import config
+from keystone import exception
+
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+# supported token versions
+V2 = 'v2.0'
+V3 = 'v3.0'
+
+
+class UnsupportedTokenVersionException(Exception):
+ """Token version is unrecognizable or unsupported."""
+ pass
+
+
+@dependency.provider('token_provider_api')
+class Manager(manager.Manager):
+ """Default pivot point for the token provider backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ """
+
+ def __init__(self):
+ # FIXME(gyee): we are deprecating CONF.signing.token_format. This code
+ # is to ensure the token provider configuration agrees with
+ # CONF.signing.token_format.
+ if ((CONF.signing.token_format == 'PKI' and
+ not CONF.token.provider.endswith('.pki.Provider')) or
+ (CONF.signing.token_format == 'UUID' and
+ not CONF.token.provider.endswith('uuid.Provider'))):
+ raise ValueError('token_format conflicts with token provider')
+
+ super(Manager, self).__init__(CONF.token.provider)
+
+
+class Provider(object):
+ """Interface description for a Token provider."""
+
+ def get_token_version(self, token_data):
+ """Return the version of the given token data.
+
+ If the given token data is unrecognizable,
+ UnsupportedTokenVersionException is raised.
+
+ """
+ raise exception.NotImplemented()
+
+ def issue_token(self, version='v3.0', **kwargs):
+ """Issue a V3 token.
+
+ For V3 tokens, 'user_id', 'method_names', must present in kwargs.
+ Optionally, kwargs may contain 'expires_at' for rescope tokens;
+ 'project_id' for project-scoped token; 'domain_id' for
+ domain-scoped token; and 'auth_context' from the authentication
+ plugins.
+
+ :param context: request context
+ :type context: dictionary
+ :param version: version of the token to be issued
+ :type version: string
+ :param kwargs: information needed for token creation. Parameters
+ may be different depending on token version.
+ :type kwargs: dictionary
+ :returns: (token_id, token_data)
+
+ """
+ raise exception.NotImplemented()
+
+ def revoke_token(self, token_id):
+ """Revoke a given token.
+
+ :param token_id: identity of the token
+ :type token_id: string
+ :returns: None.
+ """
+ raise exception.NotImplemented()
+
+ def validate_token(self, token_id, belongs_to=None, version='v3.0'):
+ """Validate the given token and return the token data.
+
+ Must raise Unauthorized exception if unable to validate token.
+
+ :param token_id: identity of the token
+ :type token_id: string
+ :param belongs_to: identity of the scoped project to validate
+ :type belongs_to: string
+ :param version: version of the token to be validated
+ :type version: string
+ :returns: token data
+ :raises: keystone.exception.Unauthorized
+
+ """
+ raise exception.NotImplemented()
+
+ def check_token(self, token_id, belongs_to=None, version='v3.0'):
+ """Check the validity of the given V3 token.
+
+ Must raise Unauthorized exception if unable to check token.
+
+ :param token_id: identity of the token
+ :type token_id: string
+ :param belongs_to: identity of the scoped project to validate
+ :type belongs_to: string
+ :param version: version of the token to check
+ :type version: string
+ :returns: None
+ :raises: keystone.exception.Unauthorized
+
+ """
diff --git a/keystone/token/providers/__init__.py b/keystone/token/providers/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone/token/providers/__init__.py
diff --git a/keystone/token/providers/pki.py b/keystone/token/providers/pki.py
new file mode 100644
index 00000000..81abe5d4
--- /dev/null
+++ b/keystone/token/providers/pki.py
@@ -0,0 +1,44 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Keystone PKI Token Provider"""
+
+import json
+
+from keystone.common import cms
+from keystone.common import environment
+from keystone.common import logging
+from keystone import config
+from keystone import exception
+from keystone.token.providers import uuid
+
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class Provider(uuid.Provider):
+ def _get_token_id(self, token_data):
+ try:
+ token_id = cms.cms_sign_token(json.dumps(token_data),
+ CONF.signing.certfile,
+ CONF.signing.keyfile)
+ return token_id
+ except environment.subprocess.CalledProcessError:
+ LOG.exception('Unable to sign token')
+ raise exception.UnexpectedError(_(
+ 'Unable to sign token.'))
diff --git a/keystone/token/providers/uuid.py b/keystone/token/providers/uuid.py
new file mode 100644
index 00000000..e1bd0b3b
--- /dev/null
+++ b/keystone/token/providers/uuid.py
@@ -0,0 +1,349 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Keystone UUID Token Provider"""
+
+from __future__ import absolute_import
+
+import sys
+import uuid
+
+from keystone.common import dependency
+from keystone.common import logging
+from keystone import config
+from keystone import exception
+from keystone.openstack.common import timeutils
+from keystone import token
+from keystone.token import provider as token_provider
+from keystone import trust
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
+
+
+@dependency.requires('catalog_api', 'identity_api')
+class V3TokenDataHelper(object):
+ """Token data helper."""
+ def __init__(self):
+ if CONF.trust.enabled:
+ self.trust_api = trust.Manager()
+
+ def _get_filtered_domain(self, domain_id):
+ domain_ref = self.identity_api.get_domain(domain_id)
+ return {'id': domain_ref['id'], 'name': domain_ref['name']}
+
+ def _get_filtered_project(self, project_id):
+ project_ref = self.identity_api.get_project(project_id)
+ filtered_project = {
+ 'id': project_ref['id'],
+ 'name': project_ref['name']}
+ filtered_project['domain'] = self._get_filtered_domain(
+ project_ref['domain_id'])
+ return filtered_project
+
+ def _populate_scope(self, token_data, domain_id, project_id):
+ if 'domain' in token_data or 'project' in token_data:
+ # scope already exist, no need to populate it again
+ return
+
+ if domain_id:
+ token_data['domain'] = self._get_filtered_domain(domain_id)
+ if project_id:
+ token_data['project'] = self._get_filtered_project(project_id)
+
+ def _get_roles_for_user(self, user_id, domain_id, project_id):
+ roles = []
+ if domain_id:
+ roles = self.identity_api.get_roles_for_user_and_domain(
+ user_id, domain_id)
+ if project_id:
+ roles = self.identity_api.get_roles_for_user_and_project(
+ user_id, project_id)
+ return [self.identity_api.get_role(role_id) for role_id in roles]
+
+ def _populate_user(self, token_data, user_id, domain_id, project_id,
+ trust):
+ if 'user' in token_data:
+ # no need to repopulate user if it already exists
+ return
+
+ user_ref = self.identity_api.get_user(user_id)
+ if CONF.trust.enabled and trust and 'OS-TRUST:trust' not in token_data:
+ trustor_user_ref = (self.identity_api.get_user(
+ trust['trustor_user_id']))
+ if not trustor_user_ref['enabled']:
+ raise exception.Forbidden(_('Trustor is disabled.'))
+ if trust['impersonation']:
+ user_ref = trustor_user_ref
+ token_data['OS-TRUST:trust'] = (
+ {
+ 'id': trust['id'],
+ 'trustor_user': {'id': trust['trustor_user_id']},
+ 'trustee_user': {'id': trust['trustee_user_id']},
+ 'impersonation': trust['impersonation']
+ })
+ filtered_user = {
+ 'id': user_ref['id'],
+ 'name': user_ref['name'],
+ 'domain': self._get_filtered_domain(user_ref['domain_id'])}
+ token_data['user'] = filtered_user
+
+ def _populate_roles(self, token_data, user_id, domain_id, project_id,
+ trust):
+ if 'roles' in token_data:
+ # no need to repopulate roles
+ return
+
+ if CONF.trust.enabled and trust:
+ token_user_id = trust['trustor_user_id']
+ token_project_id = trust['project_id']
+ #trusts do not support domains yet
+ token_domain_id = None
+ else:
+ token_user_id = user_id
+ token_project_id = project_id
+ token_domain_id = domain_id
+
+ if token_domain_id or token_project_id:
+ roles = self._get_roles_for_user(token_user_id,
+ token_domain_id,
+ token_project_id)
+ filtered_roles = []
+ if CONF.trust.enabled and trust:
+ for trust_role in trust['roles']:
+ match_roles = [x for x in roles
+ if x['id'] == trust_role['id']]
+ if match_roles:
+ filtered_roles.append(match_roles[0])
+ else:
+ raise exception.Forbidden(
+ _('Trustee have no delegated roles.'))
+ else:
+ for role in roles:
+ filtered_roles.append({'id': role['id'],
+ 'name': role['name']})
+
+ # user has no project or domain roles, therefore access denied
+ if not filtered_roles:
+ if token_project_id:
+ msg = _('User %(user_id)s have no access '
+ 'to project %(project_id)s') % {
+ 'user_id': user_id,
+ 'project_id': token_project_id}
+ else:
+ msg = _('User %(user_id)s have no access '
+ 'to domain %(domain_id)s') % {
+ 'user_id': user_id,
+ 'domain_id': token_domain_id}
+ LOG.debug(msg)
+ raise exception.Unauthorized(msg)
+
+ token_data['roles'] = filtered_roles
+
+ def _populate_service_catalog(self, token_data, user_id,
+ domain_id, project_id, trust):
+ if 'catalog' in token_data:
+ # no need to repopulate service catalog
+ return
+
+ if CONF.trust.enabled and trust:
+ user_id = trust['trustor_user_id']
+ if project_id or domain_id:
+ try:
+ service_catalog = self.catalog_api.get_v3_catalog(
+ user_id, project_id)
+ # TODO(ayoung): KVS backend needs a sample implementation
+ except exception.NotImplemented:
+ service_catalog = {}
+ # TODO(gyee): v3 service catalog is not quite completed yet
+ # TODO(ayoung): Enforce Endpoints for trust
+ token_data['catalog'] = service_catalog
+
+ def _populate_token_dates(self, token_data, expires=None, trust=None):
+ if not expires:
+ expires = token.default_expire_time()
+ if not isinstance(expires, basestring):
+ expires = timeutils.isotime(expires, subsecond=True)
+ token_data['expires_at'] = expires
+ token_data['issued_at'] = timeutils.isotime(subsecond=True)
+
+ def get_token_data(self, user_id, method_names, extras,
+ domain_id=None, project_id=None, expires=None,
+ trust=None, token=None):
+ token_data = {'methods': method_names,
+ 'extras': extras}
+
+ # We've probably already written these to the token
+ if token:
+ for x in ('roles', 'user', 'catalog', 'project', 'domain'):
+ if x in token:
+ token_data[x] = token[x]
+
+ if CONF.trust.enabled and trust:
+ if user_id != trust['trustee_user_id']:
+ raise exception.Forbidden(_('User is not a trustee.'))
+
+ self._populate_scope(token_data, domain_id, project_id)
+ self._populate_user(token_data, user_id, domain_id, project_id, trust)
+ self._populate_roles(token_data, user_id, domain_id, project_id, trust)
+ self._populate_service_catalog(token_data, user_id, domain_id,
+ project_id, trust)
+ self._populate_token_dates(token_data, expires=expires, trust=trust)
+ return {'token': token_data}
+
+
+@dependency.requires('token_api', 'identity_api')
+class Provider(token_provider.Provider):
+ def __init__(self, *args, **kwargs):
+ super(Provider, self).__init__(*args, **kwargs)
+ if CONF.trust.enabled:
+ self.trust_api = trust.Manager()
+ self.v3_token_data_helper = V3TokenDataHelper()
+
+ def get_token_version(self, token_data):
+ if token_data and isinstance(token_data, dict):
+ if 'access' in token_data:
+ return token_provider.V2
+ if 'token' in token_data and 'methods' in token_data['token']:
+ return token_provider.V3
+ raise token_provider.UnsupportedTokenVersionException()
+
+ def _get_token_id(self, token_data):
+ return uuid.uuid4().hex
+
+ def _issue_v3_token(self, **kwargs):
+ user_id = kwargs.get('user_id')
+ method_names = kwargs.get('method_names')
+ expires_at = kwargs.get('expires_at')
+ project_id = kwargs.get('project_id')
+ domain_id = kwargs.get('domain_id')
+ auth_context = kwargs.get('auth_context')
+ trust = kwargs.get('trust')
+ metadata_ref = kwargs.get('metadata_ref')
+ # for V2, trust is stashed in metadata_ref
+ if (CONF.trust.enabled and not trust and metadata_ref and
+ 'trust_id' in metadata_ref):
+ trust = self.trust_api.get_trust(metadata_ref['trust_id'])
+ token_data = self.v3_token_data_helper.get_token_data(
+ user_id,
+ method_names,
+ auth_context.get('extras') if auth_context else None,
+ domain_id=domain_id,
+ project_id=project_id,
+ expires=expires_at,
+ trust=trust)
+
+ token_id = self._get_token_id(token_data)
+ try:
+ expiry = token_data['token']['expires_at']
+ if isinstance(expiry, basestring):
+ expiry = timeutils.normalize_time(
+ timeutils.parse_isotime(expiry))
+ # FIXME(gyee): is there really a need to store roles in metadata?
+ role_ids = []
+ metadata_ref = kwargs.get('metadata_ref', {})
+ if 'project' in token_data['token']:
+ # project-scoped token, fill in the v2 token data
+ # all we care are the role IDs
+ role_ids = [r['id'] for r in token_data['token']['roles']]
+ metadata_ref = {'roles': role_ids}
+ if trust:
+ metadata_ref.setdefault('trust_id', trust['id'])
+ metadata_ref.setdefault('trustee_user_id',
+ trust['trustee_user_id'])
+ data = dict(key=token_id,
+ id=token_id,
+ expires=expiry,
+ user=token_data['token']['user'],
+ tenant=token_data['token'].get('project'),
+ metadata=metadata_ref,
+ token_data=token_data,
+ trust_id=trust['id'] if trust else None)
+ self.token_api.create_token(token_id, data)
+ except Exception:
+ exc_info = sys.exc_info()
+ # an identical token may have been created already.
+ # if so, return the token_data as it is also identical
+ try:
+ self.token_api.get_token(token_id)
+ except exception.TokenNotFound:
+ raise exc_info[0], exc_info[1], exc_info[2]
+
+ return (token_id, token_data)
+
+ def issue_token(self, version='v3.0', **kwargs):
+ if version == token_provider.V3:
+ return self._issue_v3_token(**kwargs)
+ raise token_provider.UnsupportedTokenVersionException
+
+ def _verify_token(self, token_id, belongs_to=None):
+ """Verify the given token and return the token_ref."""
+ token_ref = self.token_api.get_token(token_id=token_id)
+ assert token_ref
+ if belongs_to:
+ assert token_ref['tenant']['id'] == belongs_to
+ return token_ref
+
+ def revoke_token(self, token_id):
+ self.token_api.delete_token(token_id=token_id)
+
+ def _validate_v3_token(self, token_id):
+ token_ref = self._verify_token(token_id)
+ # FIXME(gyee): performance or correctness? Should we return the
+ # cached token or reconstruct it? Obviously if we are going with
+ # the cached token, any role, project, or domain name changes
+ # will not be reflected. One may argue that with PKI tokens,
+ # we are essentially doing cached token validation anyway.
+ # Lets go with the cached token strategy. Since token
+ # management layer is now pluggable, one can always provide
+ # their own implementation to suit their needs.
+ token_data = token_ref.get('token_data')
+ if not token_data or 'token' not in token_data:
+ # token ref is created by V2 API
+ project_id = None
+ project_ref = token_ref.get('tenant')
+ if project_ref:
+ project_id = project_ref['id']
+ token_data = self.v3_token_data_helper.get_token_data(
+ token_ref['user']['id'],
+ ['password', 'token'],
+ {},
+ project_id=project_id,
+ expires=token_ref['expires'])
+ return token_data
+
+ def validate_token(self, token_id, belongs_to=None,
+ version='v3.0'):
+ try:
+ if version == token_provider.V3:
+ return self._validate_v3_token(token_id)
+ raise token_provider.UnsupportedTokenVersionException()
+ except exception.TokenNotFound as e:
+ LOG.exception(_('Failed to verify token'))
+ raise exception.Unauthorized(e)
+
+ def check_token(self, token_id, belongs_to=None,
+ version='v3.0', **kwargs):
+ try:
+ if version == token_provider.V3:
+ self._verify_token(token_id)
+ else:
+ raise token_provider.UnsupportedTokenVersionException()
+ except exception.TokenNotFound as e:
+ LOG.exception(_('Failed to verify token'))
+ raise exception.Unauthorized(e)
diff --git a/tests/test_auth.py b/tests/test_auth.py
index b14977b9..56430d67 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -68,6 +68,10 @@ class AuthTest(test.TestCase):
self.load_backends()
self.load_fixtures(default_fixtures)
+ # need to register the token provider first because auth controller
+ # depends on it
+ token.provider.Manager()
+
self.controller = token.controllers.Auth()
def assertEqualTokens(self, a, b):
@@ -653,12 +657,12 @@ class AuthWithTrust(AuthTest):
def test_v3_trust_token_get_token_fails(self):
auth_response = self.fetch_v3_token_from_trust()
trust_token = auth_response.headers['X-Subject-Token']
- v3_token_data = {
- "methods": ["token"],
- "token": {"id": trust_token}
- }
+ v3_token_data = {'identity': {
+ 'methods': ['token'],
+ 'token': {'id': trust_token}
+ }}
self.assertRaises(
- exception.Unauthorized,
+ exception.Forbidden,
self.auth_v3_controller.authenticate_for_token,
{}, v3_token_data)
diff --git a/tests/test_auth_plugin.py b/tests/test_auth_plugin.py
index 22357471..d158ec46 100644
--- a/tests/test_auth_plugin.py
+++ b/tests/test_auth_plugin.py
@@ -20,6 +20,7 @@ from keystone import test
from keystone import auth
from keystone import exception
+from keystone import token
# for testing purposes only
@@ -49,6 +50,11 @@ class TestAuthPlugin(test.TestCase):
test.testsdir('test_auth_plugin.conf')])
self.load_backends()
auth.controllers.AUTH_METHODS[METHOD_NAME] = SimpleChallengeResponse()
+
+ # need to register the token provider first because auth controller
+ # depends on it
+ token.provider.Manager()
+
self.api = auth.controllers.Auth()
def test_unsupported_auth_method(self):
diff --git a/tests/test_backend_memcache.py b/tests/test_backend_memcache.py
index 66401e09..7516e0dd 100644
--- a/tests/test_backend_memcache.py
+++ b/tests/test_backend_memcache.py
@@ -164,7 +164,7 @@ class MemcacheToken(test.TestCase, test_backend.TokenTests):
user_token_list = jsonutils.loads('[%s]' % user_record)
self.assertEquals(len(user_token_list), 2)
expired_token_ptk = self.token_api.driver._prefix_token_id(
- token.unique_id(expired_token_id))
+ expired_token_id)
expired_token = self.token_api.driver.client.get(expired_token_ptk)
expired_token['expires'] = (timeutils.utcnow() - expire_delta)
self.token_api.driver.client.set(expired_token_ptk, expired_token)
diff --git a/tests/test_cert_setup.py b/tests/test_cert_setup.py
index 74e5466a..e6c395e9 100644
--- a/tests/test_cert_setup.py
+++ b/tests/test_cert_setup.py
@@ -61,7 +61,6 @@ class CertSetupTestCase(test.TestCase):
self.controller = token.controllers.Auth()
def test_can_handle_missing_certs(self):
- self.opt_in_group('signing', token_format='PKI')
self.opt_in_group('signing', certfile='invalid')
user = {
'id': 'fake1',
diff --git a/tests/test_pki_token_provider.conf b/tests/test_pki_token_provider.conf
new file mode 100644
index 00000000..ec8df231
--- /dev/null
+++ b/tests/test_pki_token_provider.conf
@@ -0,0 +1,5 @@
+[signing]
+token_format = PKI
+
+[token]
+provider = keystone.token.providers.pki.Provider
diff --git a/tests/test_token_provider.py b/tests/test_token_provider.py
new file mode 100644
index 00000000..7db07126
--- /dev/null
+++ b/tests/test_token_provider.py
@@ -0,0 +1,396 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from keystone import test
+from keystone import token
+
+
+SAMPLE_V2_TOKEN = {
+ "access": {
+ "trust": {
+ "id": "abc123",
+ "trustee_user_id": "123456"
+ },
+ "serviceCatalog": [
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:8774/v1.1/01257",
+ "id": "51934fe63a5b4ac0a32664f64eb462c3",
+ "internalURL": "http://localhost:8774/v1.1/01257",
+ "publicURL": "http://localhost:8774/v1.1/01257",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "nova",
+ "type": "compute"
+ },
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:9292",
+ "id": "aaa17a539e364297a7845d67c7c7cc4b",
+ "internalURL": "http://localhost:9292",
+ "publicURL": "http://localhost:9292",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "glance",
+ "type": "image"
+ },
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:8776/v1/01257",
+ "id": "077d82df25304abeac2294004441db5a",
+ "internalURL": "http://localhost:8776/v1/01257",
+ "publicURL": "http://localhost:8776/v1/01257",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "volume",
+ "type": "volume"
+ },
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:8773/services/Admin",
+ "id": "b06997fd08414903ad458836efaa9067",
+ "internalURL": "http://localhost:8773/services/Cloud",
+ "publicURL": "http://localhost:8773/services/Cloud",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "ec2",
+ "type": "ec2"
+ },
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:8888/v1",
+ "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6",
+ "internalURL": "http://localhost:8888/v1/AUTH_01257",
+ "publicURL": "http://localhost:8888/v1/AUTH_01257",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "swift",
+ "type": "object-store"
+ },
+ {
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:35357/v2.0",
+ "id": "02850c5d1d094887bdc46e81e1e15dc7",
+ "internalURL": "http://localhost:5000/v2.0",
+ "publicURL": "http://localhost:5000/v2.0",
+ "region": "RegionOne"
+ }
+ ],
+ "endpoints_links": [],
+ "name": "keystone",
+ "type": "identity"
+ }
+ ],
+ "token": {
+ "expires": "2013-05-22T00:02:43.941430Z",
+ "id": "ce4fc2d36eea4cc9a36e666ac2f1029a",
+ "issued_at": "2013-05-21T00:02:43.941473Z",
+ "tenant": {
+ "enabled": True,
+ "id": "01257",
+ "name": "service"
+ }
+ },
+ "user": {
+ "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce",
+ "name": "nova",
+ "roles": [
+ {
+ "name": "_member_"
+ },
+ {
+ "name": "admin"
+ }
+ ],
+ "roles_links": [],
+ "username": "nova"
+ }
+ }
+}
+
+SAMPLE_V3_TOKEN = {
+ "token": {
+ "catalog": [
+ {
+ "endpoints": [
+ {
+ "id": "02850c5d1d094887bdc46e81e1e15dc7",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:35357/v2.0"
+ },
+ {
+ "id": "446e244b75034a9ab4b0811e82d0b7c8",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:5000/v2.0"
+ },
+ {
+ "id": "47fa3d9f499240abb5dfcf2668f168cd",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:5000/v2.0"
+ }
+ ],
+ "id": "26d7541715a44a4d9adad96f9872b633",
+ "type": "identity",
+ },
+ {
+ "endpoints": [
+ {
+ "id": "aaa17a539e364297a7845d67c7c7cc4b",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:9292"
+ },
+ {
+ "id": "4fa9620e42394cb1974736dce0856c71",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:9292"
+ },
+ {
+ "id": "9673687f9bc441d88dec37942bfd603b",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:9292"
+ }
+ ],
+ "id": "d27a41843f4e4b0e8cf6dac4082deb0d",
+ "type": "image",
+ },
+ {
+ "endpoints": [
+ {
+ "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:8888/v1"
+ },
+ {
+ "id": "43bef154594d4ccb8e49014d20624e1d",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:8888/v1/AUTH_01257"
+ },
+ {
+ "id": "e63b5f5d7aa3493690189d0ff843b9b3",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:8888/v1/AUTH_01257"
+ }
+ ],
+ "id": "a669e152f1104810a4b6701aade721bb",
+ "type": "object-store",
+ },
+ {
+ "endpoints": [
+ {
+ "id": "51934fe63a5b4ac0a32664f64eb462c3",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:8774/v1.1/01257"
+ },
+ {
+ "id": "869b535eea0d42e483ae9da0d868ebad",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:8774/v1.1/01257"
+ },
+ {
+ "id": "93583824c18f4263a2245ca432b132a6",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:8774/v1.1/01257"
+ }
+ ],
+ "id": "7f32cc2af6c9476e82d75f80e8b3bbb8",
+ "type": "compute",
+ },
+ {
+ "endpoints": [
+ {
+ "id": "b06997fd08414903ad458836efaa9067",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:8773/services/Admin"
+ },
+ {
+ "id": "411f7de7c9a8484c9b46c254fb2676e2",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:8773/services/Cloud"
+ },
+ {
+ "id": "f21c93f3da014785854b4126d0109c49",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:8773/services/Cloud"
+ }
+ ],
+ "id": "b08c9c7d4ef543eba5eeb766f72e5aa1",
+ "type": "ec2",
+ },
+ {
+ "endpoints": [
+ {
+ "id": "077d82df25304abeac2294004441db5a",
+ "interface": "admin",
+ "region": "RegionOne",
+ "url": "http://localhost:8776/v1/01257"
+ },
+ {
+ "id": "875bf282362c40219665278b4fd11467",
+ "interface": "internal",
+ "region": "RegionOne",
+ "url": "http://localhost:8776/v1/01257"
+ },
+ {
+ "id": "cd229aa6df0640dc858a8026eb7e640c",
+ "interface": "public",
+ "region": "RegionOne",
+ "url": "http://localhost:8776/v1/01257"
+ }
+ ],
+ "id": "5db21b82617f4a95816064736a7bec22",
+ "type": "volume",
+ }
+ ],
+ "expires_at": "2013-05-22T00:02:43.941430Z",
+ "issued_at": "2013-05-21T00:02:43.941473Z",
+ "methods": [
+ "password"
+ ],
+ "project": {
+ "domain": {
+ "id": "default",
+ "name": "Default"
+ },
+ "id": "01257",
+ "name": "service"
+ },
+ "roles": [
+ {
+ "id": "9fe2ff9ee4384b1894a90878d3e92bab",
+ "name": "_member_"
+ },
+ {
+ "id": "53bff13443bd4450b97f978881d47b18",
+ "name": "admin"
+ }
+ ],
+ "user": {
+ "domain": {
+ "id": "default",
+ "name": "Default"
+ },
+ "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce",
+ "name": "nova"
+ },
+ "OS-TRUST:trust": {
+ "id": "abc123",
+ "trustee_user_id": "123456",
+ "trustor_user_id": "333333",
+ "impersonation": False
+ }
+ }
+}
+
+
+class TestTokenProvider(test.TestCase):
+ def setUp(self):
+ super(TestTokenProvider, self).setUp()
+ self.load_backends()
+ self.token_provider_api = token.provider.Manager()
+
+ def test_get_token_version(self):
+ self.assertEqual(
+ token.provider.V2,
+ self.token_provider_api.get_token_version(SAMPLE_V2_TOKEN))
+ self.assertEqual(
+ token.provider.V3,
+ self.token_provider_api.get_token_version(SAMPLE_V3_TOKEN))
+ self.assertRaises(token.provider.UnsupportedTokenVersionException,
+ self.token_provider_api.get_token_version,
+ 'bogus')
+
+ def test_issue_token(self):
+ self.assertRaises(token.provider.UnsupportedTokenVersionException,
+ self.token_provider_api.issue_token,
+ 'bogus_version')
+
+ def test_validate_token(self):
+ self.assertRaises(token.provider.UnsupportedTokenVersionException,
+ self.token_provider_api.validate_token,
+ uuid.uuid4().hex,
+ None,
+ 'bogus_version')
+
+ def test_token_format_provider_mismatch(self):
+ self.opt_in_group('signing', token_format='UUID')
+ self.opt_in_group('token',
+ provider='keystone.token.providers.pki.Provider')
+ try:
+ token.provider.Manager()
+ raise Exception(
+ 'expecting ValueError on token provider misconfiguration')
+ except ValueError:
+ pass
+
+ self.opt_in_group('signing', token_format='PKI')
+ self.opt_in_group('token',
+ provider='keystone.token.providers.uuid.Provider')
+ try:
+ token.provider.Manager()
+ raise Exception(
+ 'expecting ValueError on token provider misconfiguration')
+ except ValueError:
+ pass
+
+ # should be OK as token_format and provider aligns
+ self.opt_in_group('signing', token_format='PKI')
+ self.opt_in_group('token',
+ provider='keystone.token.providers.pki.Provider')
+ token.provider.Manager()
+
+ self.opt_in_group('signing', token_format='UUID')
+ self.opt_in_group('token',
+ provider='keystone.token.providers.uuid.Provider')
+ token.provider.Manager()
+
+ # custom provider should be OK too
+ self.opt_in_group('signing', token_format='CUSTOM')
+ self.opt_in_group('token',
+ provider='keystone.token.providers.pki.Provider')
+ token.provider.Manager()
diff --git a/tests/test_uuid_token_provider.conf b/tests/test_uuid_token_provider.conf
new file mode 100644
index 00000000..d1ac5fdf
--- /dev/null
+++ b/tests/test_uuid_token_provider.conf
@@ -0,0 +1,5 @@
+[signing]
+token_format = UUID
+
+[token]
+provider = keystone.token.providers.uuid.Provider
diff --git a/tests/test_v3.py b/tests/test_v3.py
index bf1c4d11..ff9f7596 100644
--- a/tests/test_v3.py
+++ b/tests/test_v3.py
@@ -5,6 +5,7 @@ from lxml import etree
import webtest
from keystone import test
+from keystone import token
from keystone import auth
from keystone.common import serializer
@@ -22,6 +23,15 @@ TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
class RestfulTestCase(test_content_types.RestfulTestCase):
+ _config_file_list = [test.etcdir('keystone.conf.sample'),
+ test.testsdir('test_overrides.conf'),
+ test.testsdir('backend_sql.conf'),
+ test.testsdir('backend_sql_disk.conf')]
+
+ #override this to sepcify the complete list of configuration files
+ def config_files(self):
+ return self._config_file_list
+
def setUp(self, load_sample_data=True):
"""Setup for v3 Restful Test Cases.
@@ -30,15 +40,13 @@ class RestfulTestCase(test_content_types.RestfulTestCase):
load_sample_data should be set to false.
"""
- self.config([
- test.etcdir('keystone.conf.sample'),
- test.testsdir('test_overrides.conf'),
- test.testsdir('backend_sql.conf'),
- test.testsdir('backend_sql_disk.conf')])
+ self.config(self.config_files())
test.setup_test_database()
self.load_backends()
+ self.token_provider_api = token.provider.Manager()
+
self.public_app = webtest.TestApp(
self.loadapp('keystone', name='main'))
self.admin_app = webtest.TestApp(
diff --git a/tests/test_v3_auth.py b/tests/test_v3_auth.py
index c38d13c9..8c4e4a8c 100644
--- a/tests/test_v3_auth.py
+++ b/tests/test_v3_auth.py
@@ -21,6 +21,7 @@ from keystone import auth
from keystone.common import cms
from keystone import config
from keystone import exception
+from keystone import test
import test_v3
@@ -96,9 +97,14 @@ class TestAuthInfo(test_v3.RestfulTestCase):
method_name)
-class TestTokenAPIs(test_v3.RestfulTestCase):
+class TestPKITokenAPIs(test_v3.RestfulTestCase):
+ def config_files(self):
+ conf_files = super(TestPKITokenAPIs, self).config_files()
+ conf_files.append(test.testsdir('test_pki_token_provider.conf'))
+ return conf_files
+
def setUp(self):
- super(TestTokenAPIs, self).setUp()
+ super(TestPKITokenAPIs, self).setUp()
auth_data = self.build_authentication_request(
username=self.user['name'],
user_domain_id=self.domain_id,
@@ -111,8 +117,7 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
def test_default_fixture_scope_token(self):
self.assertIsNotNone(self.get_scoped_token())
- def test_v3_pki_token_id(self):
- self.opt_in_group('signing', token_format='PKI')
+ def test_v3_token_id(self):
auth_data = self.build_authentication_request(
user_id=self.user['id'],
password=self.user['password'])
@@ -120,13 +125,19 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
token_data = resp.result
token_id = resp.headers.get('X-Subject-Token')
self.assertIn('expires_at', token_data['token'])
- token_signed = cms.cms_sign_token(json.dumps(token_data),
- CONF.signing.certfile,
- CONF.signing.keyfile)
- self.assertEqual(token_signed, token_id)
+
+ expected_token_id = cms.cms_sign_token(json.dumps(token_data),
+ CONF.signing.certfile,
+ CONF.signing.keyfile)
+ self.assertEqual(expected_token_id, token_id)
+ # should be able to validate hash PKI token as well
+ hash_token_id = cms.cms_hash_token(token_id)
+ headers = {'X-Subject-Token': hash_token_id}
+ resp = self.get('/auth/tokens', headers=headers)
+ expected_token_data = resp.result
+ self.assertDictEqual(expected_token_data, token_data)
def test_v3_v2_intermix_non_default_domain_failed(self):
- self.opt_in_group('signing', token_format='UUID')
auth_data = self.build_authentication_request(
user_id=self.user['id'],
password=self.user['password'])
@@ -141,7 +152,6 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
expected_status=401)
def test_v3_v2_intermix_domain_scoped_token_failed(self):
- self.opt_in_group('signing', token_format='UUID')
# grant the domain role to user
path = '/domains/%s/users/%s/roles/%s' % (
self.domain['id'], self.user['id'], self.role['id'])
@@ -175,8 +185,7 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
method='GET',
expected_status=401)
- def test_v3_v2_unscoped_uuid_token_intermix(self):
- self.opt_in_group('signing', token_format='UUID')
+ def test_v3_v2_unscoped_token_intermix(self):
auth_data = self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password'])
@@ -197,32 +206,9 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
self.assertIn(v2_token['access']['token']['expires'][:-1],
token_data['token']['expires_at'])
- def test_v3_v2_unscoped_pki_token_intermix(self):
- self.opt_in_group('signing', token_format='PKI')
- auth_data = self.build_authentication_request(
- user_id=self.default_domain_user['id'],
- password=self.default_domain_user['password'])
- resp = self.post('/auth/tokens', body=auth_data)
- token_data = resp.result
- token = resp.headers.get('X-Subject-Token')
-
- # now validate the v3 token with v2 API
- path = '/v2.0/tokens/%s' % (token)
- resp = self.admin_request(path=path,
- token='ADMIN',
- method='GET')
- v2_token = resp.result
- self.assertEqual(v2_token['access']['user']['id'],
- token_data['token']['user']['id'])
- # v2 token time has not fraction of second precision so
- # just need to make sure the non fraction part agrees
- self.assertIn(v2_token['access']['token']['expires'][:-1],
- token_data['token']['expires_at'])
-
- def test_v3_v2_uuid_token_intermix(self):
+ def test_v3_v2_token_intermix(self):
# FIXME(gyee): PKI tokens are not interchangeable because token
# data is baked into the token itself.
- self.opt_in_group('signing', token_format='UUID')
auth_data = self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password'],
@@ -246,10 +232,7 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
self.assertEqual(v2_token['access']['user']['roles'][0]['id'],
token_data['token']['roles'][0]['id'])
- def test_v3_v2_pki_token_intermix(self):
- # FIXME(gyee): PKI tokens are not interchangeable because token
- # data is baked into the token itself.
- self.opt_in_group('signing', token_format='PKI')
+ def test_v3_v2_hashed_pki_token_intermix(self):
auth_data = self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password'],
@@ -258,7 +241,8 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
token_data = resp.result
token = resp.headers.get('X-Subject-Token')
- # now validate the v3 token with v2 API
+ # should be able to validate a hash PKI token in v2 too
+ token = cms.cms_hash_token(token)
path = '/v2.0/tokens/%s' % (token)
resp = self.admin_request(path=path,
token='ADMIN',
@@ -268,13 +252,12 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
token_data['token']['user']['id'])
# v2 token time has not fraction of second precision so
# just need to make sure the non fraction part agrees
- self.assertIn(v2_token['access']['token']['expires'][-1],
+ self.assertIn(v2_token['access']['token']['expires'][:-1],
token_data['token']['expires_at'])
self.assertEqual(v2_token['access']['user']['roles'][0]['id'],
token_data['token']['roles'][0]['id'])
- def test_v2_v3_unscoped_uuid_token_intermix(self):
- self.opt_in_group('signing', token_format='UUID')
+ def test_v2_v3_unscoped_token_intermix(self):
body = {
'auth': {
'passwordCredentials': {
@@ -297,59 +280,7 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
self.assertIn(v2_token_data['access']['token']['expires'][-1],
token_data['token']['expires_at'])
- def test_v2_v3_unscoped_pki_token_intermix(self):
- self.opt_in_group('signing', token_format='PKI')
- body = {
- 'auth': {
- 'passwordCredentials': {
- 'userId': self.user['id'],
- 'password': self.user['password']
- }
- }}
- resp = self.admin_request(path='/v2.0/tokens',
- method='POST',
- body=body)
- v2_token_data = resp.result
- v2_token = v2_token_data['access']['token']['id']
- headers = {'X-Subject-Token': v2_token}
- resp = self.get('/auth/tokens', headers=headers)
- token_data = resp.result
- self.assertEqual(v2_token_data['access']['user']['id'],
- token_data['token']['user']['id'])
- # v2 token time has not fraction of second precision so
- # just need to make sure the non fraction part agrees
- self.assertIn(v2_token_data['access']['token']['expires'][-1],
- token_data['token']['expires_at'])
-
- def test_v2_v3_uuid_token_intermix(self):
- self.opt_in_group('signing', token_format='UUID')
- body = {
- 'auth': {
- 'passwordCredentials': {
- 'userId': self.user['id'],
- 'password': self.user['password']
- },
- 'tenantId': self.project['id']
- }}
- resp = self.admin_request(path='/v2.0/tokens',
- method='POST',
- body=body)
- v2_token_data = resp.result
- v2_token = v2_token_data['access']['token']['id']
- headers = {'X-Subject-Token': v2_token}
- resp = self.get('/auth/tokens', headers=headers)
- token_data = resp.result
- self.assertEqual(v2_token_data['access']['user']['id'],
- token_data['token']['user']['id'])
- # v2 token time has not fraction of second precision so
- # just need to make sure the non fraction part agrees
- self.assertIn(v2_token_data['access']['token']['expires'][-1],
- token_data['token']['expires_at'])
- self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'],
- token_data['token']['roles'][0]['name'])
-
- def test_v2_v3_pki_token_intermix(self):
- self.opt_in_group('signing', token_format='PKI')
+ def test_v2_v3_token_intermix(self):
body = {
'auth': {
'passwordCredentials': {
@@ -402,6 +333,28 @@ class TestTokenAPIs(test_v3.RestfulTestCase):
self.assertIn('signed', r.result)
+class TestUUIDTokenAPIs(TestPKITokenAPIs):
+ def config_files(self):
+ conf_files = super(TestUUIDTokenAPIs, self).config_files()
+ conf_files.append(test.testsdir('test_uuid_token_provider.conf'))
+ return conf_files
+
+ def test_v3_token_id(self):
+ auth_data = self.build_authentication_request(
+ user_id=self.user['id'],
+ password=self.user['password'])
+ resp = self.post('/auth/tokens', body=auth_data)
+ token_data = resp.result
+ token_id = resp.headers.get('X-Subject-Token')
+ self.assertIn('expires_at', token_data['token'])
+ self.assertFalse(cms.is_ans1_token(token_id))
+
+ def test_v3_v2_hashed_pki_token_intermix(self):
+ # this test is only applicable for PKI tokens
+ # skipping it for UUID tokens
+ pass
+
+
class TestTokenRevoking(test_v3.RestfulTestCase):
"""Test token revocation on the v3 Identity API."""