aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/auth/controllers.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/auth/controllers.py')
-rw-r--r--keystone-moon/keystone/auth/controllers.py675
1 files changed, 0 insertions, 675 deletions
diff --git a/keystone-moon/keystone/auth/controllers.py b/keystone-moon/keystone/auth/controllers.py
deleted file mode 100644
index 3e6af80f..00000000
--- a/keystone-moon/keystone/auth/controllers.py
+++ /dev/null
@@ -1,675 +0,0 @@
-# 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_log import versionutils
-from oslo_serialization import jsonutils
-from oslo_utils import importutils
-import six
-import stevedore
-
-from keystone.common import config
-from keystone.common import controller
-from keystone.common import dependency
-from keystone.common import utils
-from keystone.common import wsgi
-from keystone import exception
-from keystone.federation import constants
-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_method(method):
- plugin_name = CONF.auth.get(method) or 'default'
- namespace = 'keystone.auth.%s' % method
- try:
- driver_manager = stevedore.DriverManager(namespace, plugin_name,
- invoke_on_load=True)
- return driver_manager.driver
- except RuntimeError:
- LOG.debug('Failed to load the %s driver (%s) using stevedore, will '
- 'attempt to load using import_object instead.',
- method, plugin_name)
-
- driver = importutils.import_object(plugin_name)
-
- msg = (_(
- 'Direct import of auth plugin %(name)r is deprecated as of Liberty in '
- 'favor of its entrypoint from %(namespace)r and may be removed in '
- 'N.') %
- {'name': plugin_name, 'namespace': namespace})
- versionutils.report_deprecated_feature(LOG, msg)
-
- return driver
-
-
-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 set(CONF.auth.methods):
- AUTH_METHODS[plugin] = load_auth_method(plugin)
- 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)
-
-
-@dependency.requires('resource_api', 'trust_api')
-class AuthInfo(object):
- """Encapsulation of "auth" request."""
-
- @staticmethod
- def create(context, auth=None, scope_only=False):
- auth_info = AuthInfo(context, auth=auth)
- auth_info._validate_and_normalize_auth_data(scope_only)
- 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:
- if (CONF.resource.domain_name_url_safe == 'strict' and
- utils.is_not_url_safe(domain_name)):
- msg = _('Domain name cannot contain reserved characters.')
- raise exception.Unauthorized(message=msg)
- 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 (CONF.resource.project_name_url_safe == 'strict' and
- utils.is_not_url_safe(project_name)):
- msg = _('Project name cannot contain reserved characters.')
- raise exception.Unauthorized(message=msg)
- 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)
- 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, scope_only=False):
- """Make sure "auth" is valid.
-
- :param scope_only: If it is True, auth methods will not be
- validated but only the scope data.
- :type scope_only: boolean
- """
- # make sure "auth" exist
- if not self.auth:
- raise exception.ValidationError(attribute='auth',
- target='request body')
-
- # NOTE(chioleong): Tokenless auth does not provide auth methods,
- # we only care about using this method to validate the scope
- # information. Therefore, validating the auth methods here is
- # insignificant and we can skip it when scope_only is set to
- # true.
- if scope_only is False:
- 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 constants.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()
-
- audit_id_only = ('audit_id_only' in context['query_string'])
-
- 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'] = utils.isotime(expires)
- if audit_id_only:
- t.pop('id', None)
- data = {'revoked': tokens}
-
- if audit_id_only:
- # No need to obfuscate if no token IDs.
- return data
-
- 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: # nosec
- # 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: # nosec
- # 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)