keystone/keystone/middleware/auth.py

278 lines
11 KiB
Python

# 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 keystonemiddleware import auth_token
from oslo_log import log
from keystone.common import authorization
from keystone.common import context
from keystone.common import provider_api
from keystone.common import tokenless_auth
from keystone.common import wsgi
import keystone.conf
from keystone import exception
from keystone.federation import constants as federation_constants
from keystone.federation import utils
from keystone.i18n import _
from keystone.models import token_model
from keystone.token.providers import common
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
__all__ = ('AuthContextMiddleware',)
class AuthContextMiddleware(provider_api.ProviderAPIMixin,
auth_token.BaseAuthProtocol):
"""Build the authentication context from the request auth token."""
kwargs_to_fetch_token = True
def __init__(self, app):
bind = CONF.token.enforce_token_bind
super(AuthContextMiddleware, self).__init__(app,
log=LOG,
enforce_token_bind=bind)
def fetch_token(self, token, **kwargs):
try:
return self.token_provider_api.validate_token(token)
except exception.TokenNotFound:
raise auth_token.InvalidToken(_('Could not find token'))
def _build_tokenless_auth_context(self, request):
"""Build the authentication context.
The context is built from the attributes provided in the env,
such as certificate and scope attributes.
"""
tokenless_helper = tokenless_auth.TokenlessAuthHelper(request.environ)
(domain_id, project_id, trust_ref, unscoped) = (
tokenless_helper.get_scope())
user_ref = tokenless_helper.get_mapped_user(
project_id,
domain_id)
# NOTE(gyee): if it is an ephemeral user, the
# given X.509 SSL client cert does not need to map to
# an existing user.
if user_ref['type'] == utils.UserType.EPHEMERAL:
auth_context = {}
auth_context['group_ids'] = user_ref['group_ids']
auth_context[federation_constants.IDENTITY_PROVIDER] = (
user_ref[federation_constants.IDENTITY_PROVIDER])
auth_context[federation_constants.PROTOCOL] = (
user_ref[federation_constants.PROTOCOL])
if domain_id and project_id:
msg = _('Scoping to both domain and project is not allowed')
raise ValueError(msg)
if domain_id:
auth_context['domain_id'] = domain_id
if project_id:
auth_context['project_id'] = project_id
auth_context['roles'] = user_ref['roles']
else:
# it's the local user, so token data is needed.
token_helper = common.V3TokenDataHelper()
token_data = token_helper.get_token_data(
user_id=user_ref['id'],
method_names=[CONF.tokenless_auth.protocol],
domain_id=domain_id,
project_id=project_id)
auth_context = {'user_id': user_ref['id']}
auth_context['is_delegated_auth'] = False
if domain_id:
auth_context['domain_id'] = domain_id
if project_id:
auth_context['project_id'] = project_id
auth_context['roles'] = [role['name'] for role
in token_data['token']['roles']]
return auth_context
def _validate_trusted_issuer(self, request):
"""To further filter the certificates that are trusted.
If the config option 'trusted_issuer' is absent or does
not contain the trusted issuer DN, no certificates
will be allowed in tokenless authorization.
:param env: The env contains the client issuer's attributes
:type env: dict
:returns: True if client_issuer is trusted; otherwise False
"""
if not CONF.tokenless_auth.trusted_issuer:
return False
issuer = request.environ.get(CONF.tokenless_auth.issuer_attribute)
if not issuer:
msg = ('Cannot find client issuer in env by the '
'issuer attribute - %s.')
LOG.info(msg, CONF.tokenless_auth.issuer_attribute)
return False
if issuer in CONF.tokenless_auth.trusted_issuer:
return True
msg = ('The client issuer %(client_issuer)s does not match with '
'the trusted issuer %(trusted_issuer)s')
LOG.info(
msg, {'client_issuer': issuer,
'trusted_issuer': CONF.tokenless_auth.trusted_issuer})
return False
@wsgi.middleware_exceptions
def process_request(self, request):
context_env = request.environ.get(wsgi.CONTEXT_ENV, {})
# NOTE(notmorgan): This code is merged over from the admin token
# middleware and now emits the security warning when the
# conf.admin_token value is set.
token = request.headers.get(authorization.AUTH_TOKEN_HEADER)
if CONF.admin_token and (token == CONF.admin_token):
context_env['is_admin'] = True
LOG.warning(
"The use of the '[DEFAULT] admin_token' configuration"
"option presents a significant security risk and should "
"not be set. This option is deprecated in favor of using "
"'keystone-manage bootstrap' and will be removed in a "
"future release.")
request.environ[wsgi.CONTEXT_ENV] = context_env
if not context_env.get('is_admin', False):
resp = super(AuthContextMiddleware, self).process_request(request)
if resp:
return resp
if request.token_auth.user is not None:
request.set_user_headers(request.token_auth.user)
# NOTE(jamielennox): function is split so testing can check errors from
# fill_context. There is no actual reason for fill_context to raise
# errors rather than return a resp, simply that this is what happened
# before refactoring and it was easier to port. This can be fixed up
# and the middleware_exceptions helper removed.
self.fill_context(request)
def _keystone_specific_values(self, token, request_context):
if token.domain_scoped:
# Domain scoped tokens should never have is_admin_project set
# Even if KSA defaults it otherwise. The two mechanisms are
# parallel; only ione or the other should be used for access.
request_context.is_admin_project = False
request_context.domain_id = token.domain_id
request_context.domain_name = token.domain_name
if token.oauth_scoped:
request_context.is_delegated_auth = True
request_context.oauth_consumer_id = token.oauth_consumer_id
request_context.oauth_access_token_id = token.oauth_access_token_id
if token.trust_scoped:
request_context.is_delegated_auth = True
request_context.trust_id = token.trust_id
if token.is_federated_user:
request_context.group_ids = token.federation_group_ids
else:
request_context.group_ids = []
def fill_context(self, request):
# The request context stores itself in thread-local memory for logging.
if authorization.AUTH_CONTEXT_ENV in request.environ:
msg = ('Auth context already exists in the request '
'environment; it will be used for authorization '
'instead of creating a new one.')
LOG.warning(msg)
return
kwargs = {
'authenticated': False,
'overwrite': True}
request_context = context.RequestContext.from_environ(
request.environ, **kwargs)
request.environ[context.REQUEST_CONTEXT_ENV] = request_context
# NOTE(gyee): token takes precedence over SSL client certificates.
# This will preserve backward compatibility with the existing
# behavior. Tokenless authorization with X.509 SSL client
# certificate is effectively disabled if no trusted issuers are
# provided.
if request.environ.get(wsgi.CONTEXT_ENV, {}).get('is_admin', False):
request_context.is_admin = True
auth_context = {}
elif request.token_auth.has_user_token:
# Keystone enforces policy on some values that other services
# do not, and should not, use. This adds them in to the context.
token = token_model.KeystoneToken(token_id=request.user_token,
token_data=request.token_info)
self._keystone_specific_values(token, request_context)
request_context.auth_token = request.user_token
auth_context = request_context.to_policy_values()
additional = {
'trust_id': request_context.trust_id,
'trustor_id': request_context.trustor_id,
'trustee_id': request_context.trustee_id,
'domain_id': request_context._domain_id,
'domain_name': request_context.domain_name,
'group_ids': request_context.group_ids,
'token': token
}
auth_context.update(additional)
elif self._validate_trusted_issuer(request):
auth_context = self._build_tokenless_auth_context(request)
else:
# There is either no auth token in the request or the certificate
# issuer is not trusted. No auth context will be set. This
# typically happens on an initial token request.
return
# set authenticated to flag to keystone that a token has been validated
request_context.authenticated = True
LOG.debug('RBAC: auth_context: %s', auth_context)
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context
@classmethod
def factory(cls, global_config, **local_config):
"""Used for paste app factories in paste.deploy config files.
Any local configuration (that is, values under the [filter:APPNAME]
section of the paste config) will be passed into the `__init__` method
as kwargs.
A hypothetical configuration would look like:
[filter:analytics]
redis_host = 127.0.0.1
paste.filter_factory = keystone.analytics:Analytics.factory
which would result in a call to the `Analytics` class as
import keystone.analytics
keystone.analytics.Analytics(app, redis_host='127.0.0.1')
You could of course re-implement the `factory` method in subclasses,
but using the kwarg passing it shouldn't be necessary.
"""
def _factory(app):
conf = global_config.copy()
conf.update(local_config)
return cls(app, **local_config)
return _factory