summaryrefslogtreecommitdiffstats
path: root/keystone/auth
diff options
context:
space:
mode:
authorGuang Yee <guang.yee@hp.com>2013-01-08 08:46:20 -0800
committerGuang Yee <guang.yee@hp.com>2013-02-20 13:18:38 -0800
commit9f812939d4b05384b0a7d48e6b916baeca0477dc (patch)
treedda2e10abea730ab99955b3d595e60735b273a1f /keystone/auth
parentd036db145d51f8b134ffa36165065a8986e4f8a1 (diff)
downloadkeystone-9f812939d4b05384b0a7d48e6b916baeca0477dc.tar.gz
keystone-9f812939d4b05384b0a7d48e6b916baeca0477dc.tar.xz
keystone-9f812939d4b05384b0a7d48e6b916baeca0477dc.zip
v3 token API
Also implemented the following: blueprint pluggable-identity-authentication-handlers blueprint stop-ids-in-uris blueprint multi-factor-authn (just the plumbing) What's missing? * domain scoping (will be implemented by Henry?) Change-Id: I191c0b2cb3367b2a5f8a2dc674c284bb13ea97e3
Diffstat (limited to 'keystone/auth')
-rw-r--r--keystone/auth/__init__.py19
-rw-r--r--keystone/auth/controllers.py388
-rw-r--r--keystone/auth/core.py83
-rw-r--r--keystone/auth/methods/__init__.py0
-rw-r--r--keystone/auth/methods/password.py114
-rw-r--r--keystone/auth/methods/token.py49
-rw-r--r--keystone/auth/routers.py43
-rw-r--r--keystone/auth/token_factory.py238
8 files changed, 934 insertions, 0 deletions
diff --git a/keystone/auth/__init__.py b/keystone/auth/__init__.py
new file mode 100644
index 00000000..614f3634
--- /dev/null
+++ b/keystone/auth/__init__.py
@@ -0,0 +1,19 @@
+# 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.
+
+from keystone.auth.core import AuthMethodHandler
+from keystone.auth import controllers
+from keystone.auth import routers
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py
new file mode 100644
index 00000000..2ef4f8d1
--- /dev/null
+++ b/keystone/auth/controllers.py
@@ -0,0 +1,388 @@
+# 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 json
+
+from keystone.auth import token_factory
+from keystone.common import controller
+from keystone.common import cms
+from keystone.common import logging
+from keystone import config
+from keystone import exception
+from keystone import identity
+from keystone import token
+from keystone.openstack.common import importutils
+
+
+LOG = logging.getLogger(__name__)
+
+CONF = config.CONF
+
+# registry of authentication methods
+AUTH_METHODS = {}
+
+
+# register method drivers
+for method_name in CONF.auth.methods:
+ try:
+ config.register_str(method_name, group='auth')
+ except Exception as e:
+ # don't care about duplicate error
+ LOG.warn(e)
+
+
+def load_auth_method(method_name):
+ if method_name not in CONF.auth.methods:
+ raise exception.AuthMethodNotSupported()
+ driver = CONF.auth.get(method_name)
+ return importutils.import_object(driver)
+
+
+def get_auth_method(method_name):
+ global AUTH_METHODS
+ if method_name not in AUTH_METHODS:
+ AUTH_METHODS[method_name] = load_auth_method(method_name)
+ return AUTH_METHODS[method_name]
+
+
+class AuthInfo(object):
+ """ Encapsulation of "auth" request. """
+
+ def __init__(self, context, auth=None):
+ self.identity_api = identity.Manager()
+ self.context = context
+ self.auth = auth
+ self._scope_data = (None, None)
+ # self._scope_data is (domain_id, project_id)
+ # project scope: (None, project_id)
+ # domain scope: (domain_id, None)
+ # unscoped: (None, None)
+ self._validate_and_normalize_auth_data()
+
+ def _assert_project_is_enabled(self, project_ref):
+ # ensure the project is enabled
+ if not project_ref.get('enabled', True):
+ msg = _('Project is disabled: %s') % project_ref['id']
+ LOG.warning(msg)
+ raise exception.Unauthorized(msg)
+
+ def _assert_domain_is_enabled(self, domain_ref):
+ if not domain_ref.get('enabled'):
+ msg = _('Domain is disabled: %s') % (domain_ref['id'])
+ LOG.warning(msg)
+ raise exception.Unauthorized(msg)
+
+ def _assert_user_is_enabled(self, user_ref):
+ if not user_ref.get('enabled', True):
+ msg = _('User is disabled: %s') % (user_ref['id'])
+ LOG.warning(msg)
+ raise exception.Unauthorized(msg)
+
+ def _lookup_domain(self, domain_info):
+ domain_id = domain_info.get('id')
+ domain_name = domain_info.get('name')
+ domain_ref = None
+ if not domain_id and not domain_name:
+ raise exception.ValidationError(attribute='id or name',
+ target='domain')
+ try:
+ if domain_name:
+ domain_ref = self.identity_api.get_domain_by_name(
+ context=self.context, domain_name=domain_name)
+ else:
+ domain_ref = self.identity_api.get_domain(
+ context=self.context, domain_id=domain_id)
+ except exception.DomainNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+ self._assert_domain_is_enabled(domain_ref)
+ return domain_ref
+
+ def _lookup_project(self, project_info):
+ project_id = project_info.get('id')
+ project_name = project_info.get('name')
+ project_ref = None
+ if not project_id and not project_name:
+ raise exception.ValidationError(attribute='id or name',
+ target='project')
+ try:
+ if project_name:
+ if 'domain' not in project_info:
+ raise exception.ValidationError(attribute='domain',
+ target='project')
+ domain_ref = self._lookup_domain(project_info['domain'])
+ project_ref = self.identity_api.get_project_by_name(
+ context=self.context, tenant_name=project_name,
+ domain_id=domain_ref['id'])
+ else:
+ project_ref = self.identity_api.get_project(
+ context=self.context, tenant_id=project_id)
+ except exception.ProjectNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+ self._assert_project_is_enabled(project_ref)
+ return project_ref
+
+ def lookup_user(self, user_info):
+ user_id = user_info.get('id')
+ user_name = user_info.get('name')
+ user_ref = None
+ if not user_id and not user_name:
+ raise exception.ValidationError(attribute='id or name',
+ target='user')
+ try:
+ if user_name:
+ if 'domain' not in user_info:
+ raise exception.ValidationError(attribute='domain',
+ target='user')
+ domain_ref = self._lookup_domain(user_info['domain'])
+ user_ref = self.identity_api.get_user_by_name(
+ context=self.context, user_name=user_name,
+ domain_id=domain_ref['id'])
+ else:
+ user_ref = self.identity_api.get_user(
+ context=self.context, user_id=user_id)
+ except exception.UserNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+ self._assert_user_is_enabled(user_ref)
+ return user_ref
+
+ def _validate_and_normalize_scope_data(self):
+ """ Validate and normalize scope data """
+ if 'scope' not in self.auth:
+ return
+
+ # if scoped, only to a project or domain, but not both
+ if ('project' not in self.auth['scope'] and
+ 'domain' not in self.auth['scope']):
+ # neither domain or project provided
+ raise exception.ValidationError(attribute='project or domain',
+ target='scope')
+ if ('project' in self.auth['scope'] and
+ 'domain' in self.auth['scope']):
+ # both domain and project provided
+ raise exception.ValidationError(attribute='project or domain',
+ target='scope')
+
+ if 'project' in self.auth['scope']:
+ project_ref = self._lookup_project(self.auth['scope']['project'])
+ self._scope_data = (None, project_ref['id'])
+ else:
+ domain_ref = self._lookup_domain(self.auth['scope']['domain'])
+ self._scope_data = (domain_ref['id'], None)
+
+ def _validate_auth_methods(self):
+ # make sure auth methods are provided
+ if 'methods' not in self.auth['authentication']:
+ raise exception.ValidationError(attribute='methods',
+ target='authentication')
+
+ # make sure all the method data/payload are provided
+ for method_name in self.get_method_names():
+ if method_name not in self.auth['authentication']:
+ raise exception.ValidationError(attribute=method_name,
+ target='authentication')
+
+ # make sure auth method is supported
+ for method_name in self.get_method_names():
+ if method_name not in CONF.auth.methods:
+ raise exception.AuthMethodNotSupported()
+
+ def _validate_and_normalize_auth_data(self):
+ """ Make sure "auth" is valid. """
+ # make sure "auth" exist
+ if not self.auth:
+ raise exception.ValidationError(attribute='auth',
+ target='request body')
+
+ self._validate_auth_methods()
+ self._validate_and_normalize_scope_data()
+
+ def get_method_names(self):
+ """ Returns the authentication method names.
+
+ :returns: list of auth method names
+
+ """
+ return self.auth['authentication']['methods']
+
+ def get_method_data(self, method):
+ """ Get the auth method payload.
+
+ :returns: auth method payload
+
+ """
+ if method not in self.auth['authentication']['methods']:
+ raise exception.ValidationError(attribute=method_name,
+ target='authentication')
+ return self.auth['authentication'][method]
+
+ def get_scope(self):
+ """ Get scope information.
+
+ Verify and return the scoping information.
+
+ :returns: (domain_id, project_id). If scope to a project,
+ (None, project_id) will be returned. If scope to a domain,
+ (domain_id, None) will be returned. If unscope,
+ (None, None) will be returned.
+
+ """
+ return self._scope_data
+
+ def set_scope(self, domain_id=None, project_id=None):
+ """ Set scope information. """
+ if domain_id and project_id:
+ msg = _('Scoping to both domain and project is not allowed')
+ raise ValueError(msg)
+ self._scope_data = (domain_id, project_id)
+
+
+class Auth(controller.V3Controller):
+ def __init__(self, *args, **kw):
+ super(Auth, self).__init__(*args, **kw)
+ self.token_controllers_ref = token.controllers.Auth()
+
+ def authenticate_for_token(self, context, authentication, scope=None):
+ """ Authenticate user and issue a token. """
+ try:
+ auth = None
+ auth = {'authentication': authentication}
+ if scope:
+ auth['scope'] = scope
+ auth_info = AuthInfo(context, auth=auth)
+ auth_context = {'extras': {}, 'method_names': []}
+ self.authenticate(context, auth_info, auth_context)
+ self._check_and_set_default_scoping(context, auth_info,
+ auth_context)
+ (token_id, token_data) = token_factory.create_token(
+ context, auth_context, auth_info)
+ return token_factory.render_token_data_response(token_id,
+ token_data)
+ except (exception.Unauthorized,
+ exception.AuthMethodNotSupported,
+ exception.AdditionalAuthRequired) as e:
+ raise e
+ except Exception as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+
+ def _check_and_set_default_scoping(self, context, auth_info, auth_context):
+ (domain_id, project_id) = auth_info.get_scope()
+ if domain_id or project_id:
+ # scope is specified
+ return
+
+ # fill in default_project_id if it is available
+ try:
+ user_ref = self.identity_api.get_user(
+ context=context, user_id=auth_context['user_id'])
+ default_project_id = user_ref.get('default_project_id')
+ if default_project_id:
+ auth_info.set_scope(domain_id=None,
+ project_id=default_project_id)
+ except exception.UserNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+
+ def _build_remote_user_auth_context(self, context, auth_info,
+ auth_context):
+ username = context['REMOTE_USER']
+ # FIXME(gyee): REMOTE_USER is not good enough since we are
+ # requiring domain_id to do user lookup now. Try to get
+ # the user_id from auth_info for now, assuming external auth
+ # has check to make sure user is the same as the one specify
+ # in "authentication".
+ if 'password' in auth_info.get_method_names():
+ user_info = auth_info.get_method_data('password')
+ user_ref = auth_info.lookup_user(user_info['user'])
+ auth_context['user_id'] = user_ref['id']
+ else:
+ msg = _('Unable to lookup user %s') % (username)
+ raise exception.Unauthorized(msg)
+
+ def authenticate(self, context, auth_info, auth_context):
+ """ Authenticate user. """
+
+ # user have been authenticated externally
+ if 'REMOTE_USER' in context:
+ self._build_remote_user_auth_context(context,
+ auth_info,
+ auth_context)
+ return
+
+ # need to aggregate the results in case two or more methods
+ # are specified
+ auth_response = {'methods': []}
+ for method_name in auth_info.get_method_names():
+ method = get_auth_method(method_name)
+ resp = method.authenticate(context,
+ auth_info.get_method_data(method_name),
+ auth_context)
+ if resp:
+ auth_response['methods'].append(method_name)
+ auth_response[method_name] = resp
+
+ if len(auth_response["methods"]) > 0:
+ # authentication continuation required
+ raise exception.AdditionalAuthRequired(auth_response)
+
+ if 'user_id' not in auth_context:
+ 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(context=context,
+ token_id=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)
+
+ @controller.protected
+ def revoke_token(self, context):
+ token_id = context.get('subject_token_id')
+ return self.token_controllers_ref.delete_token(context, 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(context, token_id)
+ return token_factory.recreate_token_data(context,
+ token_ref.get('token_data'),
+ token_ref['expires'],
+ token_ref.get('user'),
+ token_ref.get('tenant'))
+
+ @controller.protected
+ def revocation_list(self, context, auth=None):
+ return self.token_controllers_ref.revocation_list(context, auth)
diff --git a/keystone/auth/core.py b/keystone/auth/core.py
new file mode 100644
index 00000000..40f7d040
--- /dev/null
+++ b/keystone/auth/core.py
@@ -0,0 +1,83 @@
+# 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.
+
+from keystone import exception
+from keystone.common import dependency
+
+
+@dependency.requires('identity_api')
+class AuthMethodHandler(object):
+ """Abstract base class for an authentication plugin."""
+
+ def __init__(self):
+ pass
+
+ def authenticate(self, context, auth_payload, auth_context):
+ """Authenticate user and return an authentication context.
+
+ :param context: keystone's request context
+ :auth_payload: the content of the authentication for a given method
+ :auth_context: user authentication context, a dictionary shared
+ by all plugins. It contains "method_names" and "extras"
+ by default. "method_names" is a list and "extras" is
+ a dictionary.
+
+ If successful, plugin must set "user_id" in "auth_context".
+ "method_name" is used to convey any additional authentication methods
+ in case authentication is for re-scoping. For example,
+ if the authentication is for re-scoping, plugin must append the
+ previous method names into "method_names". Also, plugin may add
+ any additional information into "extras". Anything in "extras"
+ will be conveyed in the token's "extras" field. Here's an example of
+ "auth_context" on successful authentication.
+
+ {"user_id": "abc123",
+ "methods": ["password", "token"],
+ "extras": {}}
+
+ Plugins are invoked in the order in which they are specified in the
+ "methods" attribute of the "authentication" request body.
+ For example, with the following authentication request,
+
+ {"authentication": {
+ "methods": ["custom-plugin", "password", "token"],
+ "token": {
+ "id": "sdfafasdfsfasfasdfds"
+ },
+ "custom-plugin": {
+ "custom-data": "sdfdfsfsfsdfsf"
+ },
+ "password": {
+ "user": {
+ "id": "s23sfad1",
+ "password": "secrete"
+ }
+ }
+ }}
+
+ plugins will be invoked in this order:
+
+ 1. custom-plugin
+ 2. password
+ 3. token
+
+ :returns: None if authentication is successful.
+ Authentication payload in the form of a dictionary for the
+ next authentication step if this is a multi step
+ authentication.
+ :raises: exception.Unauthorized for authentication failure
+ """
+ raise exception.Unauthorized()
diff --git a/keystone/auth/methods/__init__.py b/keystone/auth/methods/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone/auth/methods/__init__.py
diff --git a/keystone/auth/methods/password.py b/keystone/auth/methods/password.py
new file mode 100644
index 00000000..46ae6cb9
--- /dev/null
+++ b/keystone/auth/methods/password.py
@@ -0,0 +1,114 @@
+# 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.
+
+
+from keystone.common import logging
+from keystone import auth
+from keystone import exception
+from keystone import identity
+
+
+METHOD_NAME = 'password'
+
+LOG = logging.getLogger(__name__)
+
+
+class UserAuthInfo(object):
+ def __init__(self, context, auth_payload):
+ self.identity_api = identity.Manager()
+ self.context = context
+ self.user_id = None
+ self.password = None
+ self.user_ref = None
+ self._validate_and_normalize_auth_data(auth_payload)
+
+ def _assert_domain_is_enabled(self, domain_ref):
+ if not domain_ref.get('enabled'):
+ msg = _('Domain is disabled: %s') % (domain_ref['id'])
+ LOG.warning(msg)
+ raise exception.Unauthorized(msg)
+
+ def _assert_user_is_enabled(self, user_ref):
+ if not user_ref.get('enabled', True):
+ msg = _('User is disabled: %s') % (user_ref['id'])
+ LOG.warning(msg)
+ raise exception.Unauthorized(msg)
+
+ def _lookup_domain(self, domain_info):
+ domain_id = domain_info.get('id')
+ domain_name = domain_info.get('name')
+ domain_ref = None
+ if not domain_id and not domain_name:
+ raise exception.ValidationError(attribute='id or name',
+ target='domain')
+ try:
+ if domain_name:
+ domain_ref = self.identity_api.get_domain_by_name(
+ context=self.context, domain_name=domain_name)
+ else:
+ domain_ref = self.identity_api.get_domain(
+ context=self.context, domain_id=domain_id)
+ except exception.DomainNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+ self._assert_domain_is_enabled(domain_ref)
+ return domain_ref
+
+ def _validate_and_normalize_auth_data(self, auth_payload):
+ if 'user' not in auth_payload:
+ raise exception.ValidationError(attribute='user',
+ target=METHOD_NAME)
+ user_info = auth_payload['user']
+ user_id = user_info.get('id')
+ user_name = user_info.get('name')
+ user_ref = None
+ if not user_id and not user_name:
+ raise exception.ValidationError(attribute='id or name',
+ target='user')
+ self.password = user_info.get('password', None)
+ try:
+ if user_name:
+ if 'domain' not in user_info:
+ raise exception.ValidationError(attribute='domain',
+ target='user')
+ domain_ref = self._lookup_domain(user_info['domain'])
+ user_ref = self.identity_api.get_user_by_name(
+ context=self.context, user_name=user_name,
+ domain_id=domain_ref['id'])
+ else:
+ user_ref = self.identity_api.get_user(
+ context=self.context, user_id=user_id)
+ except exception.UserNotFound as e:
+ LOG.exception(e)
+ raise exception.Unauthorized(e)
+ self._assert_user_is_enabled(user_ref)
+ self.user_ref = user_ref
+ self.user_id = user_ref['id']
+
+
+class Password(auth.AuthMethodHandler):
+ def authenticate(self, context, auth_payload, user_context):
+ """ Try to authenticate against the identity backend. """
+ user_info = UserAuthInfo(context, auth_payload)
+
+ # FIXME: identity.authenticate() can use some refactoring since
+ # all we care is password matches
+ user_auth_data = self.identity_api.authenticate(
+ context=context,
+ user_id=user_info.user_id,
+ password=user_info.password)
+ if 'user_id' not in user_context:
+ user_context['user_id'] = user_info.user_id
diff --git a/keystone/auth/methods/token.py b/keystone/auth/methods/token.py
new file mode 100644
index 00000000..72006130
--- /dev/null
+++ b/keystone/auth/methods/token.py
@@ -0,0 +1,49 @@
+# 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.
+
+
+from keystone.common import dependency
+from keystone.common import logging
+from keystone import auth
+from keystone import exception
+from keystone import token
+
+
+METHOD_NAME = 'token'
+
+LOG = logging.getLogger(__name__)
+
+
+class Token(auth.AuthMethodHandler):
+ def __init__(self):
+ self.token_api = token.Manager()
+
+ def authenticate(self, context, auth_payload, user_context):
+ try:
+ if 'id' not in auth_payload:
+ raise exception.ValidationError(attribute='id',
+ target=METHOD_NAME)
+ token_id = auth_payload['id']
+ token_ref = self.token_api.get_token(context, token_id)
+ user_context.setdefault('user_id',
+ token_ref['token_data']['user']['id'])
+ user_context.setdefault('expires',
+ token_ref['expires'])
+ user_context['extras'].update(token_ref['token_data']['extras'])
+ user_context['method_names'] += token_ref['token_data']['methods']
+ except AssertionError as e:
+ LOG.error(e)
+ raise exception.Unauthorized(e)
diff --git a/keystone/auth/routers.py b/keystone/auth/routers.py
new file mode 100644
index 00000000..29b4fdcf
--- /dev/null
+++ b/keystone/auth/routers.py
@@ -0,0 +1,43 @@
+# 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.
+
+from keystone.auth import controllers
+from keystone.common import router
+
+
+def append_v3_routers(mapper, routers):
+ auth_controller = controllers.Auth()
+
+ mapper.connect('/auth/tokens',
+ controller=auth_controller,
+ action='authenticate_for_token',
+ conditions=dict(method=['POST']))
+ mapper.connect('/auth/tokens',
+ controller=auth_controller,
+ action='check_token',
+ conditions=dict(method=['HEAD']))
+ mapper.connect('/auth/tokens',
+ controller=auth_controller,
+ action='revoke_token',
+ conditions=dict(method=['DELETE']))
+ mapper.connect('/auth/tokens',
+ controller=auth_controller,
+ action='validate_token',
+ conditions=dict(method=['GET']))
+ mapper.connect('/auth/tokens/OS-PKI/revoked',
+ controller=auth_controller,
+ action='revocation_list',
+ conditions=dict(method=['GET']))
diff --git a/keystone/auth/token_factory.py b/keystone/auth/token_factory.py
new file mode 100644
index 00000000..e803ae79
--- /dev/null
+++ b/keystone/auth/token_factory.py
@@ -0,0 +1,238 @@
+# 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 uuid
+import webob
+
+from keystone.common import cms
+from keystone.common import logging
+from keystone.common import utils
+from keystone import catalog
+from keystone import config
+from keystone import exception
+from keystone import identity
+from keystone import token as token_module
+from keystone.openstack.common import jsonutils
+from keystone.openstack.common import timeutils
+
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class TokenDataHelper(object):
+ """Token data helper."""
+ def __init__(self, context):
+ self.identity_api = identity.Manager()
+ self.catalog_api = catalog.Manager()
+ self.context = context
+
+ def _get_filtered_domain(self, domain_id):
+ domain_ref = self.identity_api.get_domain(self.context,
+ domain_id)
+ return {'id': domain_ref['id'], 'name': domain_ref['name']}
+
+ def _populate_scope(self, token_data, domain_id, project_id):
+ if domain_id:
+ token_data['domain'] = self._get_filtered_domain(domain_id)
+ if project_id:
+ project_ref = self.identity_api.get_project(
+ self.context, 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(
+ self.context, user_id, project_id)
+ roles_ref = []
+ for role_id in roles:
+ role_ref = self.identity_api.get_role(self.context, 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_roles_for_user(self, user_id, domain_id, project_id):
+ roles = []
+ if domain_id:
+ # TODO(gyee): get domain roles
+ pass
+ 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):
+ user_ref = self.identity_api.get_user(self.context,
+ user_id)
+ 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):
+ if domain_id or project_id:
+ roles = self._get_roles_for_user(user_id, domain_id, project_id)
+ # we only care about id and name
+ filtered_roles = []
+ 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):
+ service_catalog = self.catalog_api.get_v3_catalog(self.context,
+ user_id,
+ project_id)
+ # TODO(gyee): v3 service catalog is not quite completed yet
+ token_data['catalog'] = service_catalog
+
+ def _populate_token(self, token_data, expires=None):
+ if not expires:
+ expires = token_module.default_expire_time()
+ if not isinstance(expires, unicode):
+ expires = timeutils.isotime(expires)
+ token_data['expires'] = expires
+ token_data['issued_at'] = timeutils.strtime()
+
+ def get_token_data(self, user_id, method_names, extras,
+ domain_id=None, project_id=None, expires=None):
+ token_data = {'methods': method_names,
+ 'extras': extras}
+ self._populate_scope(token_data, domain_id, project_id)
+ self._populate_user(token_data, user_id, domain_id, project_id)
+ self._populate_roles(token_data, user_id, domain_id, project_id)
+ self._populate_service_catalog(token_data, user_id, domain_id,
+ project_id)
+ self._populate_token(token_data, expires)
+ return token_data
+
+
+def recreate_token_data(context, 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 = {}
+ if token_data:
+ domain_id = (token_data['domain']['id'] if 'domain' in token_data
+ else None)
+ project_id = (token_data['project']['id'] if 'project' in token_data
+ else None)
+ if not new_expires:
+ new_expires = token_data['expires']
+ user_id = token_data['user']['id']
+ methods = token_data['methods']
+ extras = token_data['extras']
+ else:
+ project_id = project_ref['id']
+ user_id = user_ref['id']
+ token_data_helper = TokenDataHelper(context)
+ return token_data_helper.get_token_data(user_id,
+ methods,
+ extras,
+ domain_id,
+ project_id,
+ new_expires)
+
+
+def create_token(context, auth_context, auth_info):
+ token_data_helper = TokenDataHelper(context)
+ (domain_id, project_id) = 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',
+ None))
+ if CONF.signing.token_format == 'UUID':
+ token_id = uuid.uuid4().hex
+ elif CONF.signing.token_format == 'PKI':
+ token_id = cms.cms_sign_token(json.dumps(token_data),
+ CONF.signing.certfile,
+ CONF.signing.keyfile)
+ 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['expires']
+ if isinstance(expiry, basestring):
+ expiry = timeutils.parse_isotime(expiry)
+ role_ids = []
+ if 'project' in token_data:
+ # 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['roles']]
+ metadata_ref = {'roles': role_ids}
+ data = dict(key=token_id,
+ id=token_id,
+ expires=expiry,
+ user=token_data['user'],
+ tenant=token_data.get('project'),
+ metadata=metadata_ref,
+ token_data=token_data)
+ token_api.create_token(context, token_id, data)
+ except Exception as e:
+ # an identical token may have been created already.
+ # if so, return the token_data as it is also identical
+ try:
+ token_api.get_token(context=context,
+ token_id=token_id)
+ except exception.TokenNotFound:
+ raise e
+
+ return (token_id, token_data)
+
+
+def render_token_data_response(token_id, token_data):
+ """ 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'))
+ status = (200, 'OK')
+ body = jsonutils.dumps(token_data, cls=utils.SmarterEncoder)
+ return webob.Response(body=body,
+ status='%s %s' % status,
+ headerlist=headers)