aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/auth
diff options
context:
space:
mode:
authorWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
committerWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
commitb8c756ecdd7cced1db4300935484e8c83701c82e (patch)
tree87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/auth
parentc304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff)
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/auth')
-rw-r--r--keystone-moon/keystone/auth/__init__.py17
-rw-r--r--keystone-moon/keystone/auth/controllers.py647
-rw-r--r--keystone-moon/keystone/auth/core.py94
-rw-r--r--keystone-moon/keystone/auth/plugins/__init__.py15
-rw-r--r--keystone-moon/keystone/auth/plugins/core.py186
-rw-r--r--keystone-moon/keystone/auth/plugins/external.py186
-rw-r--r--keystone-moon/keystone/auth/plugins/mapped.py252
-rw-r--r--keystone-moon/keystone/auth/plugins/oauth1.py75
-rw-r--r--keystone-moon/keystone/auth/plugins/password.py49
-rw-r--r--keystone-moon/keystone/auth/plugins/saml2.py27
-rw-r--r--keystone-moon/keystone/auth/plugins/token.py99
-rw-r--r--keystone-moon/keystone/auth/routers.py57
12 files changed, 1704 insertions, 0 deletions
diff --git a/keystone-moon/keystone/auth/__init__.py b/keystone-moon/keystone/auth/__init__.py
new file mode 100644
index 00000000..b1e4203e
--- /dev/null
+++ b/keystone-moon/keystone/auth/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 # noqa
+from keystone.auth.core import * # noqa
+from keystone.auth import routers # noqa
diff --git a/keystone-moon/keystone/auth/controllers.py b/keystone-moon/keystone/auth/controllers.py
new file mode 100644
index 00000000..065f1f01
--- /dev/null
+++ b/keystone-moon/keystone/auth/controllers.py
@@ -0,0 +1,647 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 sys
+
+from keystoneclient.common import cms
+from oslo_config import cfg
+from oslo_log import log
+from oslo_serialization import jsonutils
+from oslo_utils import importutils
+from oslo_utils import timeutils
+import six
+
+from keystone.common import controller
+from keystone.common import dependency
+from keystone.common import wsgi
+from keystone import config
+from keystone.contrib import federation
+from keystone import exception
+from keystone.i18n import _, _LI, _LW
+from keystone.resource import controllers as resource_controllers
+
+
+LOG = log.getLogger(__name__)
+
+CONF = cfg.CONF
+
+# registry of authentication methods
+AUTH_METHODS = {}
+AUTH_PLUGINS_LOADED = False
+
+
+def load_auth_methods():
+ global AUTH_PLUGINS_LOADED
+
+ if AUTH_PLUGINS_LOADED:
+ # Only try and load methods a single time.
+ return
+ # config.setup_authentication should be idempotent, call it to ensure we
+ # have setup all the appropriate configuration options we may need.
+ config.setup_authentication()
+ for plugin in CONF.auth.methods:
+ if '.' in plugin:
+ # NOTE(morganfainberg): if '.' is in the plugin name, it should be
+ # imported rather than used as a plugin identifier.
+ plugin_class = plugin
+ driver = importutils.import_object(plugin)
+ if not hasattr(driver, 'method'):
+ raise ValueError(_('Cannot load an auth-plugin by class-name '
+ 'without a "method" attribute defined: %s'),
+ plugin_class)
+
+ LOG.info(_LI('Loading auth-plugins by class-name is deprecated.'))
+ plugin_name = driver.method
+ else:
+ plugin_name = plugin
+ plugin_class = CONF.auth.get(plugin)
+ driver = importutils.import_object(plugin_class)
+ if plugin_name in AUTH_METHODS:
+ raise ValueError(_('Auth plugin %(plugin)s is requesting '
+ 'previously registered method %(method)s') %
+ {'plugin': plugin_class, 'method': driver.method})
+ AUTH_METHODS[plugin_name] = driver
+ AUTH_PLUGINS_LOADED = True
+
+
+def get_auth_method(method_name):
+ global AUTH_METHODS
+ if method_name not in AUTH_METHODS:
+ raise exception.AuthMethodNotSupported()
+ return AUTH_METHODS[method_name]
+
+
+class AuthContext(dict):
+ """Retrofitting auth_context to reconcile identity attributes.
+
+ The identity attributes must not have conflicting values among the
+ auth plug-ins. The only exception is `expires_at`, which is set to its
+ earliest value.
+
+ """
+
+ # identity attributes need to be reconciled among the auth plugins
+ IDENTITY_ATTRIBUTES = frozenset(['user_id', 'project_id',
+ 'access_token_id', 'domain_id',
+ 'expires_at'])
+
+ def __setitem__(self, key, val):
+ if key in self.IDENTITY_ATTRIBUTES and key in self:
+ existing_val = self[key]
+ if key == 'expires_at':
+ # special treatment for 'expires_at', we are going to take
+ # the earliest expiration instead.
+ if existing_val != val:
+ LOG.info(_LI('"expires_at" has conflicting values '
+ '%(existing)s and %(new)s. Will use the '
+ 'earliest value.'),
+ {'existing': existing_val, 'new': val})
+ if existing_val is None or val is None:
+ val = existing_val or val
+ else:
+ val = min(existing_val, val)
+ elif existing_val != val:
+ msg = _('Unable to reconcile identity attribute %(attribute)s '
+ 'as it has conflicting values %(new)s and %(old)s') % (
+ {'attribute': key,
+ 'new': val,
+ 'old': existing_val})
+ raise exception.Unauthorized(msg)
+ return super(AuthContext, self).__setitem__(key, val)
+
+
+# TODO(blk-u): this class doesn't use identity_api directly, but makes it
+# available for consumers. Consumers should probably not be getting
+# identity_api from this since it's available in global registry, then
+# identity_api should be removed from this list.
+@dependency.requires('identity_api', 'resource_api', 'trust_api')
+class AuthInfo(object):
+ """Encapsulation of "auth" request."""
+
+ @staticmethod
+ def create(context, auth=None):
+ auth_info = AuthInfo(context, auth=auth)
+ auth_info._validate_and_normalize_auth_data()
+ return auth_info
+
+ def __init__(self, context, auth=None):
+ self.context = context
+ self.auth = auth
+ self._scope_data = (None, None, None, None)
+ # self._scope_data is (domain_id, project_id, trust_ref, unscoped)
+ # project scope: (None, project_id, None, None)
+ # domain scope: (domain_id, None, None, None)
+ # trust scope: (None, None, trust_ref, None)
+ # unscoped: (None, None, None, 'unscoped')
+
+ def _assert_project_is_enabled(self, project_ref):
+ # ensure the project is enabled
+ try:
+ self.resource_api.assert_project_enabled(
+ project_id=project_ref['id'],
+ project=project_ref)
+ except AssertionError as e:
+ LOG.warning(six.text_type(e))
+ six.reraise(exception.Unauthorized, exception.Unauthorized(e),
+ sys.exc_info()[2])
+
+ def _assert_domain_is_enabled(self, domain_ref):
+ try:
+ self.resource_api.assert_domain_enabled(
+ domain_id=domain_ref['id'],
+ domain=domain_ref)
+ except AssertionError as e:
+ LOG.warning(six.text_type(e))
+ six.reraise(exception.Unauthorized, exception.Unauthorized(e),
+ sys.exc_info()[2])
+
+ 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.resource_api.get_domain_by_name(
+ domain_name)
+ else:
+ domain_ref = self.resource_api.get_domain(domain_id)
+ except exception.DomainNotFound as e:
+ LOG.exception(six.text_type(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.resource_api.get_project_by_name(
+ project_name, domain_ref['id'])
+ else:
+ project_ref = self.resource_api.get_project(project_id)
+ # NOTE(morganfainberg): The _lookup_domain method will raise
+ # exception.Unauthorized if the domain isn't found or is
+ # disabled.
+ self._lookup_domain({'id': project_ref['domain_id']})
+ except exception.ProjectNotFound as e:
+ raise exception.Unauthorized(e)
+ self._assert_project_is_enabled(project_ref)
+ return project_ref
+
+ def _lookup_trust(self, trust_info):
+ trust_id = trust_info.get('id')
+ if not trust_id:
+ raise exception.ValidationError(attribute='trust_id',
+ target='trust')
+ trust = self.trust_api.get_trust(trust_id)
+ if not trust:
+ raise exception.TrustNotFound(trust_id=trust_id)
+ return trust
+
+ def _validate_and_normalize_scope_data(self):
+ """Validate and normalize scope data."""
+ if 'scope' not in self.auth:
+ return
+ if sum(['project' in self.auth['scope'],
+ 'domain' in self.auth['scope'],
+ 'unscoped' in self.auth['scope'],
+ 'OS-TRUST:trust' in self.auth['scope']]) != 1:
+ raise exception.ValidationError(
+ attribute='project, domain, OS-TRUST:trust or unscoped',
+ target='scope')
+ if 'unscoped' in self.auth['scope']:
+ self._scope_data = (None, None, None, 'unscoped')
+ return
+ if 'project' in self.auth['scope']:
+ project_ref = self._lookup_project(self.auth['scope']['project'])
+ self._scope_data = (None, project_ref['id'], None, None)
+ elif 'domain' in self.auth['scope']:
+ domain_ref = self._lookup_domain(self.auth['scope']['domain'])
+ self._scope_data = (domain_ref['id'], None, None, None)
+ elif 'OS-TRUST:trust' in self.auth['scope']:
+ if not CONF.trust.enabled:
+ raise exception.Forbidden('Trusts are disabled.')
+ trust_ref = self._lookup_trust(
+ self.auth['scope']['OS-TRUST:trust'])
+ # TODO(ayoung): when trusts support domains, fill in domain data
+ if trust_ref.get('project_id') is not None:
+ project_ref = self._lookup_project(
+ {'id': trust_ref['project_id']})
+ self._scope_data = (None, project_ref['id'], trust_ref, None)
+ else:
+ self._scope_data = (None, None, trust_ref, None)
+
+ 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',
+ target='identity')
+
+ # make sure all the method data/payload are provided
+ for method_name in self.get_method_names():
+ if method_name not in self.auth['identity']:
+ raise exception.ValidationError(attribute=method_name,
+ target='identity')
+
+ # make sure auth method is supported
+ for method_name in self.get_method_names():
+ if method_name not in 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 identity method names.
+
+ :returns: list of auth method names
+
+ """
+ # Sanitizes methods received in request's body
+ # Filters out duplicates, while keeping elements' order.
+ method_names = []
+ for method in self.auth['identity']['methods']:
+ if method not in method_names:
+ method_names.append(method)
+ return method_names
+
+ def get_method_data(self, method):
+ """Get the auth method payload.
+
+ :returns: auth method payload
+
+ """
+ if method not in self.auth['identity']['methods']:
+ raise exception.ValidationError(attribute=method,
+ target='identity')
+ return self.auth['identity'][method]
+
+ def get_scope(self):
+ """Get scope information.
+
+ Verify and return the scoping information.
+
+ :returns: (domain_id, project_id, trust_ref, unscoped).
+ If scope to a project, (None, project_id, None, None)
+ will be returned.
+ If scoped to a domain, (domain_id, None, None, None)
+ will be returned.
+ If scoped to a trust, (None, project_id, trust_ref, None),
+ Will be returned, where the project_id comes from the
+ trust definition.
+ If unscoped, (None, None, None, 'unscoped') will be
+ returned.
+
+ """
+ return self._scope_data
+
+ def set_scope(self, domain_id=None, project_id=None, trust=None,
+ unscoped=None):
+ """Set scope information."""
+ if domain_id and project_id:
+ msg = _('Scoping to both domain and project is not allowed')
+ raise ValueError(msg)
+ if domain_id and trust:
+ msg = _('Scoping to both domain and trust is not allowed')
+ raise ValueError(msg)
+ if project_id and trust:
+ msg = _('Scoping to both project and trust is not allowed')
+ raise ValueError(msg)
+ self._scope_data = (domain_id, project_id, trust, unscoped)
+
+
+@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
+ 'resource_api', 'token_provider_api', 'trust_api')
+class Auth(controller.V3Controller):
+
+ # Note(atiwari): From V3 auth controller code we are
+ # calling protection() wrappers, so we need to setup
+ # the member_name and collection_name attributes of
+ # auth controller code.
+ # In the absence of these attributes, default 'entity'
+ # string will be used to represent the target which is
+ # generic. Policy can be defined using 'entity' but it
+ # would not reflect the exact entity that is in context.
+ # We are defining collection_name = 'tokens' and
+ # member_name = 'token' to facilitate policy decisions.
+ collection_name = 'tokens'
+ member_name = 'token'
+
+ def __init__(self, *args, **kw):
+ super(Auth, self).__init__(*args, **kw)
+ config.setup_authentication()
+
+ def authenticate_for_token(self, context, auth=None):
+ """Authenticate user and issue a token."""
+ include_catalog = 'nocatalog' not in context['query_string']
+
+ try:
+ auth_info = AuthInfo.create(context, auth=auth)
+ auth_context = AuthContext(extras={},
+ method_names=[],
+ bind={})
+ self.authenticate(context, auth_info, auth_context)
+ if auth_context.get('access_token_id'):
+ auth_info.set_scope(None, auth_context['project_id'], None)
+ self._check_and_set_default_scoping(auth_info, auth_context)
+ (domain_id, project_id, trust, unscoped) = 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))
+ expires_at = auth_context.get('expires_at')
+ # NOTE(morganfainberg): define this here so it is clear what the
+ # argument is during the issue_v3_token provider call.
+ metadata_ref = None
+
+ token_audit_id = auth_context.get('audit_id')
+
+ (token_id, token_data) = self.token_provider_api.issue_v3_token(
+ auth_context['user_id'], method_names, expires_at, project_id,
+ domain_id, auth_context, trust, metadata_ref, include_catalog,
+ parent_audit_id=token_audit_id)
+
+ # NOTE(wanghong): We consume a trust use only when we are using
+ # trusts and have successfully issued a token.
+ if trust:
+ self.trust_api.consume_use(trust['id'])
+
+ 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):
+ (domain_id, project_id, trust, unscoped) = auth_info.get_scope()
+ if trust:
+ project_id = trust['project_id']
+ if domain_id or project_id or trust:
+ # scope is specified
+ return
+
+ # Skip scoping when unscoped federated token is being issued
+ if federation.IDENTITY_PROVIDER in auth_context:
+ return
+
+ # Do not scope if request is for explicitly unscoped token
+ if unscoped is not None:
+ return
+
+ # fill in default_project_id if it is available
+ try:
+ user_ref = self.identity_api.get_user(auth_context['user_id'])
+ except exception.UserNotFound as e:
+ LOG.exception(six.text_type(e))
+ raise exception.Unauthorized(e)
+
+ default_project_id = user_ref.get('default_project_id')
+ if not default_project_id:
+ # User has no default project. He shall get an unscoped token.
+ return
+
+ # make sure user's default project is legit before scoping to it
+ try:
+ default_project_ref = self.resource_api.get_project(
+ default_project_id)
+ default_project_domain_ref = self.resource_api.get_domain(
+ default_project_ref['domain_id'])
+ if (default_project_ref.get('enabled', True) and
+ default_project_domain_ref.get('enabled', True)):
+ if self.assignment_api.get_roles_for_user_and_project(
+ user_ref['id'], default_project_id):
+ auth_info.set_scope(project_id=default_project_id)
+ else:
+ msg = _LW("User %(user_id)s doesn't have access to"
+ " default project %(project_id)s. The token"
+ " will be unscoped rather than scoped to the"
+ " project.")
+ LOG.warning(msg,
+ {'user_id': user_ref['id'],
+ 'project_id': default_project_id})
+ else:
+ msg = _LW("User %(user_id)s's default project %(project_id)s"
+ " is disabled. The token will be unscoped rather"
+ " than scoped to the project.")
+ LOG.warning(msg,
+ {'user_id': user_ref['id'],
+ 'project_id': default_project_id})
+ except (exception.ProjectNotFound, exception.DomainNotFound):
+ # default project or default project domain doesn't exist,
+ # will issue unscoped token instead
+ msg = _LW("User %(user_id)s's default project %(project_id)s not"
+ " found. The token will be unscoped rather than"
+ " scoped to the project.")
+ LOG.warning(msg, {'user_id': user_ref['id'],
+ 'project_id': default_project_id})
+
+ def authenticate(self, context, auth_info, auth_context):
+ """Authenticate user."""
+
+ # The 'external' method allows any 'REMOTE_USER' based authentication
+ # In some cases the server can set REMOTE_USER as '' instead of
+ # dropping it, so this must be filtered out
+ if context['environment'].get('REMOTE_USER'):
+ try:
+ external = get_auth_method('external')
+ external.authenticate(context, auth_info, auth_context)
+ except exception.AuthMethodNotSupported:
+ # This will happen there is no 'external' plugin registered
+ # and the container is performing authentication.
+ # The 'kerberos' and 'saml' methods will be used this way.
+ # In those cases, it is correct to not register an
+ # 'external' plugin; if there is both an 'external' and a
+ # 'kerberos' plugin, it would run the check on identity twice.
+ LOG.debug("No 'external' plugin is registered.")
+ except exception.Unauthorized:
+ # If external fails then continue and attempt to determine
+ # user identity using remaining auth methods
+ LOG.debug("Authorization failed for 'external' auth method.")
+
+ # 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 auth_response["methods"]:
+ # authentication continuation required
+ raise exception.AdditionalAuthRequired(auth_response)
+
+ if 'user_id' not in auth_context:
+ msg = _('User not found')
+ raise exception.Unauthorized(msg)
+
+ @controller.protected()
+ def check_token(self, context):
+ token_id = context.get('subject_token_id')
+ token_data = self.token_provider_api.validate_v3_token(
+ token_id)
+ # NOTE(morganfainberg): The code in
+ # ``keystone.common.wsgi.render_response`` will remove the content
+ # body.
+ return render_token_data_response(token_id, token_data)
+
+ @controller.protected()
+ def revoke_token(self, context):
+ token_id = context.get('subject_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')
+ include_catalog = 'nocatalog' not in context['query_string']
+ token_data = self.token_provider_api.validate_v3_token(
+ token_id)
+ if not include_catalog and 'catalog' in token_data['token']:
+ del token_data['token']['catalog']
+ return render_token_data_response(token_id, token_data)
+
+ @controller.protected()
+ def revocation_list(self, context, auth=None):
+ if not CONF.token.revoke_by_id:
+ raise exception.Gone()
+ tokens = self.token_provider_api.list_revoked_tokens()
+
+ for t in tokens:
+ expires = t['expires']
+ if not (expires and isinstance(expires, six.text_type)):
+ t['expires'] = timeutils.isotime(expires)
+ data = {'revoked': tokens}
+ json_data = jsonutils.dumps(data)
+ signed_text = cms.cms_sign_text(json_data,
+ CONF.signing.certfile,
+ CONF.signing.keyfile)
+
+ return {'signed': signed_text}
+
+ def _combine_lists_uniquely(self, a, b):
+ # it's most likely that only one of these will be filled so avoid
+ # the combination if possible.
+ if a and b:
+ return {x['id']: x for x in a + b}.values()
+ else:
+ return a or b
+
+ @controller.protected()
+ def get_auth_projects(self, context):
+ auth_context = self.get_auth_context(context)
+
+ user_id = auth_context.get('user_id')
+ user_refs = []
+ if user_id:
+ try:
+ user_refs = self.assignment_api.list_projects_for_user(user_id)
+ except exception.UserNotFound:
+ # federated users have an id but they don't link to anything
+ pass
+
+ group_ids = auth_context.get('group_ids')
+ grp_refs = []
+ if group_ids:
+ grp_refs = self.assignment_api.list_projects_for_groups(group_ids)
+
+ refs = self._combine_lists_uniquely(user_refs, grp_refs)
+ return resource_controllers.ProjectV3.wrap_collection(context, refs)
+
+ @controller.protected()
+ def get_auth_domains(self, context):
+ auth_context = self.get_auth_context(context)
+
+ user_id = auth_context.get('user_id')
+ user_refs = []
+ if user_id:
+ try:
+ user_refs = self.assignment_api.list_domains_for_user(user_id)
+ except exception.UserNotFound:
+ # federated users have an id but they don't link to anything
+ pass
+
+ group_ids = auth_context.get('group_ids')
+ grp_refs = []
+ if group_ids:
+ grp_refs = self.assignment_api.list_domains_for_groups(group_ids)
+
+ refs = self._combine_lists_uniquely(user_refs, grp_refs)
+ return resource_controllers.DomainV3.wrap_collection(context, refs)
+
+ @controller.protected()
+ def get_auth_catalog(self, context):
+ auth_context = self.get_auth_context(context)
+ user_id = auth_context.get('user_id')
+ project_id = auth_context.get('project_id')
+
+ if not project_id:
+ raise exception.Forbidden(
+ _('A project-scoped token is required to produce a service '
+ 'catalog.'))
+
+ # The V3Controller base methods mostly assume that you're returning
+ # either a collection or a single element from a collection, neither of
+ # which apply to the catalog. Because this is a special case, this
+ # re-implements a tiny bit of work done by the base controller (such as
+ # self-referential link building) to avoid overriding or refactoring
+ # several private methods.
+ return {
+ 'catalog': self.catalog_api.get_v3_catalog(user_id, project_id),
+ 'links': {'self': self.base_url(context, path='auth/catalog')}
+ }
+
+
+# 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-moon/keystone/auth/core.py b/keystone-moon/keystone/auth/core.py
new file mode 100644
index 00000000..9da2c123
--- /dev/null
+++ b/keystone-moon/keystone/auth/core.py
@@ -0,0 +1,94 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 abc
+
+import six
+
+from keystone import exception
+
+
+@six.add_metaclass(abc.ABCMeta)
+class AuthMethodHandler(object):
+ """Abstract base class for an authentication plugin."""
+
+ def __init__(self):
+ pass
+
+ @abc.abstractmethod
+ def authenticate(self, context, auth_payload, auth_context):
+ """Authenticate user and return an authentication context.
+
+ :param context: keystone's request context
+ :param auth_payload: the content of the authentication for a given
+ method
+ :param 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`` attribute. Here's an example of ``auth_context``
+ on successful authentication::
+
+ {
+ "extras": {},
+ "methods": [
+ "password",
+ "token"
+ ],
+ "user_id": "abc123"
+ }
+
+ Plugins are invoked in the order in which they are specified in the
+ ``methods`` attribute of the ``identity`` object. For example,
+ ``custom-plugin`` is invoked before ``password``, which is invoked
+ before ``token`` in the following authentication request::
+
+ {
+ "auth": {
+ "identity": {
+ "custom-plugin": {
+ "custom-data": "sdfdfsfsfsdfsf"
+ },
+ "methods": [
+ "custom-plugin",
+ "password",
+ "token"
+ ],
+ "password": {
+ "user": {
+ "id": "s23sfad1",
+ "password": "secrete"
+ }
+ },
+ "token": {
+ "id": "sdfafasdfsfasfasdfds"
+ }
+ }
+ }
+ }
+
+ :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-moon/keystone/auth/plugins/__init__.py b/keystone-moon/keystone/auth/plugins/__init__.py
new file mode 100644
index 00000000..5da54703
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2015 CERN
+#
+# 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.plugins.core import * # noqa
diff --git a/keystone-moon/keystone/auth/plugins/core.py b/keystone-moon/keystone/auth/plugins/core.py
new file mode 100644
index 00000000..96a5ecf8
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/core.py
@@ -0,0 +1,186 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 sys
+
+from oslo_config import cfg
+from oslo_log import log
+import six
+
+from keystone.common import dependency
+from keystone import exception
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+def construct_method_map_from_config():
+ """Determine authentication method types for deployment.
+
+ :returns: a dictionary containing the methods and their indexes
+
+ """
+ method_map = dict()
+ method_index = 1
+ for method in CONF.auth.methods:
+ method_map[method_index] = method
+ method_index = method_index * 2
+
+ return method_map
+
+
+def convert_method_list_to_integer(methods):
+ """Convert the method type(s) to an integer.
+
+ :param methods: a list of method names
+ :returns: an integer representing the methods
+
+ """
+ method_map = construct_method_map_from_config()
+
+ method_ints = []
+ for method in methods:
+ for k, v in six.iteritems(method_map):
+ if v == method:
+ method_ints.append(k)
+ return sum(method_ints)
+
+
+def convert_integer_to_method_list(method_int):
+ """Convert an integer to a list of methods.
+
+ :param method_int: an integer representing methods
+ :returns: a corresponding list of methods
+
+ """
+ # If the method_int is 0 then no methods were used so return an empty
+ # method list
+ if method_int == 0:
+ return []
+
+ method_map = construct_method_map_from_config()
+ method_ints = []
+ for k, v in six.iteritems(method_map):
+ method_ints.append(k)
+ method_ints.sort(reverse=True)
+
+ confirmed_methods = []
+ for m_int in method_ints:
+ # (lbragstad): By dividing the method_int by each key in the
+ # method_map, we know if the division results in an integer of 1, that
+ # key was used in the construction of the total sum of the method_int.
+ # In that case, we should confirm the key value and store it so we can
+ # look it up later. Then we should take the remainder of what is
+ # confirmed and the method_int and continue the process. In the end, we
+ # should have a list of integers that correspond to indexes in our
+ # method_map and we can reinflate the methods that the original
+ # method_int represents.
+ if (method_int / m_int) == 1:
+ confirmed_methods.append(m_int)
+ method_int = method_int - m_int
+
+ methods = []
+ for method in confirmed_methods:
+ methods.append(method_map[method])
+
+ return methods
+
+
+@dependency.requires('identity_api', 'resource_api')
+class UserAuthInfo(object):
+
+ @staticmethod
+ def create(auth_payload, method_name):
+ user_auth_info = UserAuthInfo()
+ user_auth_info._validate_and_normalize_auth_data(auth_payload)
+ user_auth_info.METHOD_NAME = method_name
+ return user_auth_info
+
+ def __init__(self):
+ self.user_id = None
+ self.password = None
+ self.user_ref = None
+ self.METHOD_NAME = None
+
+ def _assert_domain_is_enabled(self, domain_ref):
+ try:
+ self.resource_api.assert_domain_enabled(
+ domain_id=domain_ref['id'],
+ domain=domain_ref)
+ except AssertionError as e:
+ LOG.warning(six.text_type(e))
+ six.reraise(exception.Unauthorized, exception.Unauthorized(e),
+ sys.exc_info()[2])
+
+ def _assert_user_is_enabled(self, user_ref):
+ try:
+ self.identity_api.assert_user_enabled(
+ user_id=user_ref['id'],
+ user=user_ref)
+ except AssertionError as e:
+ LOG.warning(six.text_type(e))
+ six.reraise(exception.Unauthorized, exception.Unauthorized(e),
+ sys.exc_info()[2])
+
+ 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.resource_api.get_domain_by_name(
+ domain_name)
+ else:
+ domain_ref = self.resource_api.get_domain(domain_id)
+ except exception.DomainNotFound as e:
+ LOG.exception(six.text_type(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=self.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')
+ 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(
+ user_name, domain_ref['id'])
+ else:
+ user_ref = self.identity_api.get_user(user_id)
+ domain_ref = self.resource_api.get_domain(
+ user_ref['domain_id'])
+ self._assert_domain_is_enabled(domain_ref)
+ except exception.UserNotFound as e:
+ LOG.exception(six.text_type(e))
+ raise exception.Unauthorized(e)
+ self._assert_user_is_enabled(user_ref)
+ self.user_ref = user_ref
+ self.user_id = user_ref['id']
+ self.domain_id = domain_ref['id']
diff --git a/keystone-moon/keystone/auth/plugins/external.py b/keystone-moon/keystone/auth/plugins/external.py
new file mode 100644
index 00000000..2322649f
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/external.py
@@ -0,0 +1,186 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 External Authentication Plugins"""
+
+import abc
+
+from oslo_config import cfg
+import six
+
+from keystone import auth
+from keystone.common import dependency
+from keystone import exception
+from keystone.i18n import _
+from keystone.openstack.common import versionutils
+
+
+CONF = cfg.CONF
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Base(auth.AuthMethodHandler):
+
+ method = 'external'
+
+ def authenticate(self, context, auth_info, auth_context):
+ """Use REMOTE_USER to look up the user in the identity backend.
+
+ auth_context is an in-out variable that will be updated with the
+ user_id from the actual user from the REMOTE_USER env variable.
+ """
+ try:
+ REMOTE_USER = context['environment']['REMOTE_USER']
+ except KeyError:
+ msg = _('No authenticated user')
+ raise exception.Unauthorized(msg)
+ try:
+ user_ref = self._authenticate(REMOTE_USER, context)
+ auth_context['user_id'] = user_ref['id']
+ if ('kerberos' in CONF.token.bind and
+ (context['environment'].get('AUTH_TYPE', '').lower()
+ == 'negotiate')):
+ auth_context['bind']['kerberos'] = user_ref['name']
+ except Exception:
+ msg = _('Unable to lookup user %s') % (REMOTE_USER)
+ raise exception.Unauthorized(msg)
+
+ @abc.abstractmethod
+ def _authenticate(self, remote_user, context):
+ """Look up the user in the identity backend.
+
+ Return user_ref
+ """
+ pass
+
+
+@dependency.requires('identity_api')
+class DefaultDomain(Base):
+ def _authenticate(self, remote_user, context):
+ """Use remote_user to look up the user in the identity backend."""
+ domain_id = CONF.identity.default_domain_id
+ user_ref = self.identity_api.get_user_by_name(remote_user, domain_id)
+ return user_ref
+
+
+@dependency.requires('identity_api', 'resource_api')
+class Domain(Base):
+ def _authenticate(self, remote_user, context):
+ """Use remote_user to look up the user in the identity backend.
+
+ The domain will be extracted from the REMOTE_DOMAIN environment
+ variable if present. If not, the default domain will be used.
+ """
+
+ username = remote_user
+ try:
+ domain_name = context['environment']['REMOTE_DOMAIN']
+ except KeyError:
+ domain_id = CONF.identity.default_domain_id
+ else:
+ domain_ref = self.resource_api.get_domain_by_name(domain_name)
+ domain_id = domain_ref['id']
+
+ user_ref = self.identity_api.get_user_by_name(username, domain_id)
+ return user_ref
+
+
+@dependency.requires('assignment_api', 'identity_api')
+class KerberosDomain(Domain):
+ """Allows `kerberos` as a method."""
+ method = 'kerberos'
+
+ def _authenticate(self, remote_user, context):
+ auth_type = context['environment'].get('AUTH_TYPE')
+ if auth_type != 'Negotiate':
+ raise exception.Unauthorized(_("auth_type is not Negotiate"))
+ return super(KerberosDomain, self)._authenticate(remote_user, context)
+
+
+class ExternalDefault(DefaultDomain):
+ """Deprecated. Please use keystone.auth.external.DefaultDomain instead."""
+
+ @versionutils.deprecated(
+ as_of=versionutils.deprecated.ICEHOUSE,
+ in_favor_of='keystone.auth.external.DefaultDomain',
+ remove_in=+1)
+ def __init__(self):
+ super(ExternalDefault, self).__init__()
+
+
+class ExternalDomain(Domain):
+ """Deprecated. Please use keystone.auth.external.Domain instead."""
+
+ @versionutils.deprecated(
+ as_of=versionutils.deprecated.ICEHOUSE,
+ in_favor_of='keystone.auth.external.Domain',
+ remove_in=+1)
+ def __init__(self):
+ super(ExternalDomain, self).__init__()
+
+
+@dependency.requires('identity_api')
+class LegacyDefaultDomain(Base):
+ """Deprecated. Please use keystone.auth.external.DefaultDomain instead.
+
+ This plugin exists to provide compatibility for the unintended behavior
+ described here: https://bugs.launchpad.net/keystone/+bug/1253484
+
+ """
+
+ @versionutils.deprecated(
+ as_of=versionutils.deprecated.ICEHOUSE,
+ in_favor_of='keystone.auth.external.DefaultDomain',
+ remove_in=+1)
+ def __init__(self):
+ super(LegacyDefaultDomain, self).__init__()
+
+ def _authenticate(self, remote_user, context):
+ """Use remote_user to look up the user in the identity backend."""
+ # NOTE(dolph): this unintentionally discards half the REMOTE_USER value
+ names = remote_user.split('@')
+ username = names.pop(0)
+ domain_id = CONF.identity.default_domain_id
+ user_ref = self.identity_api.get_user_by_name(username, domain_id)
+ return user_ref
+
+
+@dependency.requires('identity_api', 'resource_api')
+class LegacyDomain(Base):
+ """Deprecated. Please use keystone.auth.external.Domain instead."""
+
+ @versionutils.deprecated(
+ as_of=versionutils.deprecated.ICEHOUSE,
+ in_favor_of='keystone.auth.external.Domain',
+ remove_in=+1)
+ def __init__(self):
+ super(LegacyDomain, self).__init__()
+
+ def _authenticate(self, remote_user, context):
+ """Use remote_user to look up the user in the identity backend.
+
+ If remote_user contains an `@` assume that the substring before the
+ rightmost `@` is the username, and the substring after the @ is the
+ domain name.
+ """
+ names = remote_user.rsplit('@', 1)
+ username = names.pop(0)
+ if names:
+ domain_name = names[0]
+ domain_ref = self.resource_api.get_domain_by_name(domain_name)
+ domain_id = domain_ref['id']
+ else:
+ domain_id = CONF.identity.default_domain_id
+ user_ref = self.identity_api.get_user_by_name(username, domain_id)
+ return user_ref
diff --git a/keystone-moon/keystone/auth/plugins/mapped.py b/keystone-moon/keystone/auth/plugins/mapped.py
new file mode 100644
index 00000000..abf44481
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/mapped.py
@@ -0,0 +1,252 @@
+# 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 functools
+
+from oslo_log import log
+from oslo_serialization import jsonutils
+from pycadf import cadftaxonomy as taxonomy
+from six.moves.urllib import parse
+
+from keystone import auth
+from keystone.auth import plugins as auth_plugins
+from keystone.common import dependency
+from keystone.contrib import federation
+from keystone.contrib.federation import utils
+from keystone import exception
+from keystone.i18n import _
+from keystone.models import token_model
+from keystone import notifications
+
+
+LOG = log.getLogger(__name__)
+
+METHOD_NAME = 'mapped'
+
+
+@dependency.requires('assignment_api', 'federation_api', 'identity_api',
+ 'token_provider_api')
+class Mapped(auth.AuthMethodHandler):
+
+ def _get_token_ref(self, auth_payload):
+ token_id = auth_payload['id']
+ response = self.token_provider_api.validate_token(token_id)
+ return token_model.KeystoneToken(token_id=token_id,
+ token_data=response)
+
+ def authenticate(self, context, auth_payload, auth_context):
+ """Authenticate mapped user and return an authentication context.
+
+ :param context: keystone's request context
+ :param auth_payload: the content of the authentication for a
+ given method
+ :param auth_context: user authentication context, a dictionary
+ shared by all plugins.
+
+ In addition to ``user_id`` in ``auth_context``, this plugin sets
+ ``group_ids``, ``OS-FEDERATION:identity_provider`` and
+ ``OS-FEDERATION:protocol``
+
+ """
+
+ if 'id' in auth_payload:
+ token_ref = self._get_token_ref(auth_payload)
+ handle_scoped_token(context, auth_payload, auth_context, token_ref,
+ self.federation_api,
+ self.identity_api,
+ self.token_provider_api)
+ else:
+ handle_unscoped_token(context, auth_payload, auth_context,
+ self.assignment_api, self.federation_api,
+ self.identity_api)
+
+
+def handle_scoped_token(context, auth_payload, auth_context, token_ref,
+ federation_api, identity_api, token_provider_api):
+ utils.validate_expiration(token_ref)
+ token_audit_id = token_ref.audit_id
+ identity_provider = token_ref.federation_idp_id
+ protocol = token_ref.federation_protocol_id
+ user_id = token_ref.user_id
+ group_ids = token_ref.federation_group_ids
+ send_notification = functools.partial(
+ notifications.send_saml_audit_notification, 'authenticate',
+ context, user_id, group_ids, identity_provider, protocol,
+ token_audit_id)
+
+ utils.assert_enabled_identity_provider(federation_api, identity_provider)
+
+ try:
+ mapping = federation_api.get_mapping_from_idp_and_protocol(
+ identity_provider, protocol)
+ utils.validate_groups(group_ids, mapping['id'], identity_api)
+
+ except Exception:
+ # NOTE(topol): Diaper defense to catch any exception, so we can
+ # send off failed authentication notification, raise the exception
+ # after sending the notification
+ send_notification(taxonomy.OUTCOME_FAILURE)
+ raise
+ else:
+ send_notification(taxonomy.OUTCOME_SUCCESS)
+
+ auth_context['user_id'] = user_id
+ auth_context['group_ids'] = group_ids
+ auth_context[federation.IDENTITY_PROVIDER] = identity_provider
+ auth_context[federation.PROTOCOL] = protocol
+
+
+def handle_unscoped_token(context, auth_payload, auth_context,
+ assignment_api, federation_api, identity_api):
+
+ def is_ephemeral_user(mapped_properties):
+ return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL
+
+ def build_ephemeral_user_context(auth_context, user, mapped_properties,
+ identity_provider, protocol):
+ auth_context['user_id'] = user['id']
+ auth_context['group_ids'] = mapped_properties['group_ids']
+ auth_context[federation.IDENTITY_PROVIDER] = identity_provider
+ auth_context[federation.PROTOCOL] = protocol
+
+ def build_local_user_context(auth_context, mapped_properties):
+ user_info = auth_plugins.UserAuthInfo.create(mapped_properties,
+ METHOD_NAME)
+ auth_context['user_id'] = user_info.user_id
+
+ assertion = extract_assertion_data(context)
+ identity_provider = auth_payload['identity_provider']
+ protocol = auth_payload['protocol']
+
+ utils.assert_enabled_identity_provider(federation_api, identity_provider)
+
+ group_ids = None
+ # NOTE(topol): The user is coming in from an IdP with a SAML assertion
+ # instead of from a token, so we set token_id to None
+ token_id = None
+ # NOTE(marek-denis): This variable is set to None and there is a
+ # possibility that it will be used in the CADF notification. This means
+ # operation will not be mapped to any user (even ephemeral).
+ user_id = None
+
+ try:
+ mapped_properties = apply_mapping_filter(
+ identity_provider, protocol, assertion, assignment_api,
+ federation_api, identity_api)
+
+ if is_ephemeral_user(mapped_properties):
+ user = setup_username(context, mapped_properties)
+ user_id = user['id']
+ group_ids = mapped_properties['group_ids']
+ mapping = federation_api.get_mapping_from_idp_and_protocol(
+ identity_provider, protocol)
+ utils.validate_groups_cardinality(group_ids, mapping['id'])
+ build_ephemeral_user_context(auth_context, user,
+ mapped_properties,
+ identity_provider, protocol)
+ else:
+ build_local_user_context(auth_context, mapped_properties)
+
+ except Exception:
+ # NOTE(topol): Diaper defense to catch any exception, so we can
+ # send off failed authentication notification, raise the exception
+ # after sending the notification
+ outcome = taxonomy.OUTCOME_FAILURE
+ notifications.send_saml_audit_notification('authenticate', context,
+ user_id, group_ids,
+ identity_provider,
+ protocol, token_id,
+ outcome)
+ raise
+ else:
+ outcome = taxonomy.OUTCOME_SUCCESS
+ notifications.send_saml_audit_notification('authenticate', context,
+ user_id, group_ids,
+ identity_provider,
+ protocol, token_id,
+ outcome)
+
+
+def extract_assertion_data(context):
+ assertion = dict(utils.get_assertion_params_from_env(context))
+ return assertion
+
+
+def apply_mapping_filter(identity_provider, protocol, assertion,
+ assignment_api, federation_api, identity_api):
+ idp = federation_api.get_idp(identity_provider)
+ utils.validate_idp(idp, assertion)
+ mapping = federation_api.get_mapping_from_idp_and_protocol(
+ identity_provider, protocol)
+ rules = jsonutils.loads(mapping['rules'])
+ LOG.debug('using the following rules: %s', rules)
+ rule_processor = utils.RuleProcessor(rules)
+ mapped_properties = rule_processor.process(assertion)
+
+ # NOTE(marek-denis): We update group_ids only here to avoid fetching
+ # groups identified by name/domain twice.
+ # NOTE(marek-denis): Groups are translated from name/domain to their
+ # corresponding ids in the auth plugin, as we need information what
+ # ``mapping_id`` was used as well as idenity_api and assignment_api
+ # objects.
+ group_ids = mapped_properties['group_ids']
+ utils.validate_groups_in_backend(group_ids,
+ mapping['id'],
+ identity_api)
+ group_ids.extend(
+ utils.transform_to_group_ids(
+ mapped_properties['group_names'], mapping['id'],
+ identity_api, assignment_api))
+ mapped_properties['group_ids'] = list(set(group_ids))
+ return mapped_properties
+
+
+def setup_username(context, mapped_properties):
+ """Setup federated username.
+
+ Function covers all the cases for properly setting user id, a primary
+ identifier for identity objects. Initial version of the mapping engine
+ assumed user is identified by ``name`` and his ``id`` is built from the
+ name. We, however need to be able to accept local rules that identify user
+ by either id or name/domain.
+
+ The following use-cases are covered:
+
+ 1) If neither user_name nor user_id is set raise exception.Unauthorized
+ 2) If user_id is set and user_name not, set user_name equal to user_id
+ 3) If user_id is not set and user_name is, set user_id as url safe version
+ of user_name.
+
+ :param context: authentication context
+ :param mapped_properties: Properties issued by a RuleProcessor.
+ :type: dictionary
+
+ :raises: exception.Unauthorized
+ :returns: dictionary with user identification
+ :rtype: dict
+
+ """
+ user = mapped_properties['user']
+
+ user_id = user.get('id')
+ user_name = user.get('name') or context['environment'].get('REMOTE_USER')
+
+ if not any([user_id, user_name]):
+ raise exception.Unauthorized(_("Could not map user"))
+
+ elif not user_name:
+ user['name'] = user_id
+
+ elif not user_id:
+ user['id'] = parse.quote(user_name)
+
+ return user
diff --git a/keystone-moon/keystone/auth/plugins/oauth1.py b/keystone-moon/keystone/auth/plugins/oauth1.py
new file mode 100644
index 00000000..2f1cc2fa
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/oauth1.py
@@ -0,0 +1,75 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 oslo_log import log
+from oslo_utils import timeutils
+
+from keystone import auth
+from keystone.common import controller
+from keystone.common import dependency
+from keystone.contrib.oauth1 import core as oauth
+from keystone.contrib.oauth1 import validator
+from keystone import exception
+from keystone.i18n import _
+
+
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('oauth_api')
+class OAuth(auth.AuthMethodHandler):
+
+ method = 'oauth1'
+
+ def authenticate(self, context, auth_info, auth_context):
+ """Turn a signed request with an access key into a keystone token."""
+
+ if not self.oauth_api:
+ raise exception.Unauthorized(_('%s not supported') % self.method)
+
+ headers = context['headers']
+ oauth_headers = oauth.get_oauth_headers(headers)
+ access_token_id = oauth_headers.get('oauth_token')
+
+ if not access_token_id:
+ raise exception.ValidationError(
+ attribute='oauth_token', target='request')
+
+ acc_token = self.oauth_api.get_access_token(access_token_id)
+
+ expires_at = acc_token['expires_at']
+ if expires_at:
+ now = timeutils.utcnow()
+ expires = timeutils.normalize_time(
+ timeutils.parse_isotime(expires_at))
+ if now > expires:
+ raise exception.Unauthorized(_('Access token is expired'))
+
+ url = controller.V3Controller.base_url(context, context['path'])
+ access_verifier = oauth.ResourceEndpoint(
+ request_validator=validator.OAuthValidator(),
+ token_generator=oauth.token_generator)
+ result, request = access_verifier.validate_protected_resource_request(
+ url,
+ http_method='POST',
+ body=context['query_string'],
+ headers=headers,
+ realms=None
+ )
+ if not result:
+ msg = _('Could not validate the access token')
+ raise exception.Unauthorized(msg)
+ auth_context['user_id'] = acc_token['authorizing_user_id']
+ auth_context['access_token_id'] = access_token_id
+ auth_context['project_id'] = acc_token['project_id']
diff --git a/keystone-moon/keystone/auth/plugins/password.py b/keystone-moon/keystone/auth/plugins/password.py
new file mode 100644
index 00000000..c5770445
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/password.py
@@ -0,0 +1,49 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 oslo_log import log
+
+from keystone import auth
+from keystone.auth import plugins as auth_plugins
+from keystone.common import dependency
+from keystone import exception
+from keystone.i18n import _
+
+METHOD_NAME = 'password'
+
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('identity_api')
+class Password(auth.AuthMethodHandler):
+
+ method = METHOD_NAME
+
+ def authenticate(self, context, auth_payload, auth_context):
+ """Try to authenticate against the identity backend."""
+ user_info = auth_plugins.UserAuthInfo.create(auth_payload, self.method)
+
+ # FIXME(gyee): identity.authenticate() can use some refactoring since
+ # all we care is password matches
+ try:
+ self.identity_api.authenticate(
+ context,
+ 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)
+
+ auth_context['user_id'] = user_info.user_id
diff --git a/keystone-moon/keystone/auth/plugins/saml2.py b/keystone-moon/keystone/auth/plugins/saml2.py
new file mode 100644
index 00000000..744f26a9
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/saml2.py
@@ -0,0 +1,27 @@
+# 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.plugins import mapped
+
+""" Provide an entry point to authenticate with SAML2
+
+This plugin subclasses mapped.Mapped, and may be specified in keystone.conf:
+
+ [auth]
+ methods = external,password,token,saml2
+ saml2 = keystone.auth.plugins.mapped.Mapped
+"""
+
+
+class Saml2(mapped.Mapped):
+
+ method = 'saml2'
diff --git a/keystone-moon/keystone/auth/plugins/token.py b/keystone-moon/keystone/auth/plugins/token.py
new file mode 100644
index 00000000..5ca0b257
--- /dev/null
+++ b/keystone-moon/keystone/auth/plugins/token.py
@@ -0,0 +1,99 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 oslo_config import cfg
+from oslo_log import log
+import six
+
+from keystone import auth
+from keystone.auth.plugins import mapped
+from keystone.common import dependency
+from keystone.common import wsgi
+from keystone import exception
+from keystone.i18n import _
+from keystone.models import token_model
+
+
+LOG = log.getLogger(__name__)
+
+CONF = cfg.CONF
+
+
+@dependency.requires('federation_api', 'identity_api', 'token_provider_api')
+class Token(auth.AuthMethodHandler):
+
+ method = 'token'
+
+ def _get_token_ref(self, auth_payload):
+ token_id = auth_payload['id']
+ response = self.token_provider_api.validate_token(token_id)
+ return token_model.KeystoneToken(token_id=token_id,
+ token_data=response)
+
+ def authenticate(self, context, auth_payload, user_context):
+ if 'id' not in auth_payload:
+ raise exception.ValidationError(attribute='id',
+ target=self.method)
+ token_ref = self._get_token_ref(auth_payload)
+ if token_ref.is_federated_user and self.federation_api:
+ mapped.handle_scoped_token(
+ context, auth_payload, user_context, token_ref,
+ self.federation_api, self.identity_api,
+ self.token_provider_api)
+ else:
+ token_authenticate(context, auth_payload, user_context, token_ref)
+
+
+def token_authenticate(context, auth_payload, user_context, token_ref):
+ try:
+
+ # Do not allow tokens used for delegation to
+ # create another token, or perform any changes of
+ # state in Keystone. To do so is to invite elevation of
+ # privilege attacks
+
+ if token_ref.oauth_scoped or token_ref.trust_scoped:
+ raise exception.Forbidden()
+
+ if not CONF.token.allow_rescope_scoped_token:
+ # Do not allow conversion from scoped tokens.
+ if token_ref.project_scoped or token_ref.domain_scoped:
+ raise exception.Forbidden(action=_("rescope a scoped token"))
+
+ wsgi.validate_token_bind(context, token_ref)
+
+ # New tokens maintain the audit_id of the original token in the
+ # chain (if possible) as the second element in the audit data
+ # structure. Look for the last element in the audit data structure
+ # which will be either the audit_id of the token (in the case of
+ # a token that has not been rescoped) or the audit_chain id (in
+ # the case of a token that has been rescoped).
+ try:
+ token_audit_id = token_ref.get('audit_ids', [])[-1]
+ except IndexError:
+ # NOTE(morganfainberg): In the case this is a token that was
+ # issued prior to audit id existing, the chain is not tracked.
+ token_audit_id = None
+
+ user_context.setdefault('expires_at', token_ref.expires)
+ user_context['audit_id'] = token_audit_id
+ user_context.setdefault('user_id', token_ref.user_id)
+ # TODO(morganfainberg: determine if token 'extras' can be removed
+ # from the user_context
+ user_context['extras'].update(token_ref.get('extras', {}))
+ user_context['method_names'].extend(token_ref.methods)
+
+ except AssertionError as e:
+ LOG.error(six.text_type(e))
+ raise exception.Unauthorized(e)
diff --git a/keystone-moon/keystone/auth/routers.py b/keystone-moon/keystone/auth/routers.py
new file mode 100644
index 00000000..c7a525c3
--- /dev/null
+++ b/keystone-moon/keystone/auth/routers.py
@@ -0,0 +1,57 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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 json_home
+from keystone.common import wsgi
+
+
+class Routers(wsgi.RoutersBase):
+
+ def append_v3_routers(self, mapper, routers):
+ auth_controller = controllers.Auth()
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/tokens',
+ get_action='validate_token',
+ head_action='check_token',
+ post_action='authenticate_for_token',
+ delete_action='revoke_token',
+ rel=json_home.build_v3_resource_relation('auth_tokens'))
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/tokens/OS-PKI/revoked',
+ get_action='revocation_list',
+ rel=json_home.build_v3_extension_resource_relation(
+ 'OS-PKI', '1.0', 'revocations'))
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/catalog',
+ get_action='get_auth_catalog',
+ rel=json_home.build_v3_resource_relation('auth_catalog'))
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/projects',
+ get_action='get_auth_projects',
+ rel=json_home.build_v3_resource_relation('auth_projects'))
+
+ self._add_resource(
+ mapper, auth_controller,
+ path='/auth/domains',
+ get_action='get_auth_domains',
+ rel=json_home.build_v3_resource_relation('auth_domains'))