Merge "Make AuthContext depend on auth_token middleware"

This commit is contained in:
Jenkins 2016-05-19 02:38:09 +00:00 committed by Gerrit Code Review
commit d21edb4715
3 changed files with 131 additions and 97 deletions

View File

@ -19,6 +19,7 @@
"""Utility methods for working with WSGI servers.""" """Utility methods for working with WSGI servers."""
import copy import copy
import functools
import itertools import itertools
import re import re
import wsgiref.util import wsgiref.util
@ -395,6 +396,30 @@ class Application(BaseApplication):
return url.rstrip('/') return url.rstrip('/')
def middleware_exceptions(method):
@functools.wraps(method)
def _inner(self, request):
try:
return method(self, request)
except exception.Error as e:
LOG.warning(six.text_type(e))
return render_exception(e, request=request,
user_locale=best_match_language(request))
except TypeError as e:
LOG.exception(six.text_type(e))
return render_exception(exception.ValidationError(e),
request=request,
user_locale=best_match_language(request))
except Exception as e:
LOG.exception(six.text_type(e))
return render_exception(exception.UnexpectedError(exception=e),
request=request,
user_locale=best_match_language(request))
return _inner
class Middleware(Application): class Middleware(Application):
"""Base WSGI middleware. """Base WSGI middleware.
@ -431,27 +456,13 @@ class Middleware(Application):
return response return response
@webob.dec.wsgify() @webob.dec.wsgify()
@middleware_exceptions
def __call__(self, request): def __call__(self, request):
try: response = self.process_request(request)
response = self.process_request(request) if response:
if response: return response
return response response = request.get_response(self.application)
response = request.get_response(self.application) return self.process_response(request, response)
return self.process_response(request, response)
except exception.Error as e:
LOG.warning(six.text_type(e))
return render_exception(e, request=request,
user_locale=best_match_language(request))
except TypeError as e:
LOG.exception(six.text_type(e))
return render_exception(exception.ValidationError(e),
request=request,
user_locale=best_match_language(request))
except Exception as e:
LOG.exception(six.text_type(e))
return render_exception(exception.UnexpectedError(exception=e),
request=request,
user_locale=best_match_language(request))
class Debug(Middleware): class Debug(Middleware):

View File

@ -10,12 +10,14 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from keystonemiddleware import auth_token
from oslo_config import cfg from oslo_config import cfg
from oslo_context import context as oslo_context from oslo_context import context as oslo_context
from oslo_log import log from oslo_log import log
from oslo_log import versionutils from oslo_log import versionutils
from keystone.common import authorization from keystone.common import authorization
from keystone.common import dependency
from keystone.common import tokenless_auth from keystone.common import tokenless_auth
from keystone.common import wsgi from keystone.common import wsgi
from keystone import exception from keystone import exception
@ -32,83 +34,32 @@ LOG = log.getLogger(__name__)
__all__ = ('AuthContextMiddleware',) __all__ = ('AuthContextMiddleware',)
class AuthContextMiddleware(wsgi.Middleware): @dependency.requires('token_provider_api')
class AuthContextMiddleware(auth_token.BaseAuthProtocol):
"""Build the authentication context from the request auth token.""" """Build the authentication context from the request auth token."""
def _build_auth_context(self, request): def __init__(self, app):
bind = CONF.token.enforce_token_bind
super(AuthContextMiddleware, self).__init__(app,
log=LOG,
enforce_token_bind=bind)
# NOTE(gyee): token takes precedence over SSL client certificates. def fetch_token(self, token):
# This will preserve backward compatibility with the existing if CONF.admin_token and token == CONF.admin_token:
# behavior. Tokenless authorization with X.509 SSL client return {}
# certificate is effectively disabled if no trusted issuers are
# provided.
token_id = None
if core.AUTH_TOKEN_HEADER in request.headers:
token_id = request.headers[core.AUTH_TOKEN_HEADER].strip()
is_admin = request.environ.get(core.CONTEXT_ENV, {}).get('is_admin',
False)
if is_admin:
# NOTE(gyee): no need to proceed any further as we already know
# this is an admin request.
auth_context = {}
return auth_context, token_id, is_admin
if token_id:
# In this case the client sent in a token.
auth_context, is_admin = self._build_token_auth_context(
request, token_id)
return auth_context, token_id, is_admin
# No token, maybe the client presented an X.509 certificate.
if self._validate_trusted_issuer(request.environ):
auth_context = self._build_tokenless_auth_context(
request.environ)
return auth_context, None, False
LOG.debug('There is either no auth token in the request or '
'the certificate issuer is not trusted. No auth '
'context will be set.')
return None, None, False
def _build_token_auth_context(self, request, token_id):
if CONF.admin_token and token_id == CONF.admin_token:
versionutils.report_deprecated_feature(
LOG,
_LW('build_auth_context middleware checking for the admin '
'token is deprecated as of the Mitaka release and will be '
'removed in the O release. If your deployment requires '
'use of the admin token, update keystone-paste.ini so '
'that admin_token_auth is before build_auth_context in '
'the paste pipelines, otherwise remove the '
'admin_token_auth middleware from the paste pipelines.'))
return {}, True
context = {'token_id': token_id}
context['environment'] = request.environ
try: try:
token_ref = token_model.KeystoneToken( return self.token_provider_api.validate_token(token)
token_id=token_id,
token_data=self.token_provider_api.validate_token(token_id))
# TODO(gyee): validate_token_bind should really be its own
# middleware
wsgi.validate_token_bind(context, token_ref)
return authorization.token_to_auth_context(token_ref), False
except exception.TokenNotFound: except exception.TokenNotFound:
LOG.warning(_LW('RBAC: Invalid token')) raise auth_token.InvalidToken(_('Could not find token'))
raise exception.Unauthorized()
def _build_tokenless_auth_context(self, env): def _build_tokenless_auth_context(self, request):
"""Build the authentication context. """Build the authentication context.
The context is built from the attributes provided in the env, The context is built from the attributes provided in the env,
such as certificate and scope attributes. such as certificate and scope attributes.
""" """
tokenless_helper = tokenless_auth.TokenlessAuthHelper(env) tokenless_helper = tokenless_auth.TokenlessAuthHelper(request.environ)
(domain_id, project_id, trust_ref, unscoped) = ( (domain_id, project_id, trust_ref, unscoped) = (
tokenless_helper.get_scope()) tokenless_helper.get_scope())
@ -153,7 +104,7 @@ class AuthContextMiddleware(wsgi.Middleware):
in token_data['token']['roles']] in token_data['token']['roles']]
return auth_context return auth_context
def _validate_trusted_issuer(self, env): def _validate_trusted_issuer(self, request):
"""To further filter the certificates that are trusted. """To further filter the certificates that are trusted.
If the config option 'trusted_issuer' is absent or does If the config option 'trusted_issuer' is absent or does
@ -167,26 +118,39 @@ class AuthContextMiddleware(wsgi.Middleware):
if not CONF.tokenless_auth.trusted_issuer: if not CONF.tokenless_auth.trusted_issuer:
return False return False
client_issuer = env.get(CONF.tokenless_auth.issuer_attribute) issuer = request.environ.get(CONF.tokenless_auth.issuer_attribute)
if not client_issuer: if not issuer:
msg = _LI('Cannot find client issuer in env by the ' msg = _LI('Cannot find client issuer in env by the '
'issuer attribute - %s.') 'issuer attribute - %s.')
LOG.info(msg, CONF.tokenless_auth.issuer_attribute) LOG.info(msg, CONF.tokenless_auth.issuer_attribute)
return False return False
if client_issuer in CONF.tokenless_auth.trusted_issuer: if issuer in CONF.tokenless_auth.trusted_issuer:
return True return True
msg = _LI('The client issuer %(client_issuer)s does not match with ' msg = _LI('The client issuer %(client_issuer)s does not match with '
'the trusted issuer %(trusted_issuer)s') 'the trusted issuer %(trusted_issuer)s')
LOG.info( LOG.info(
msg, {'client_issuer': client_issuer, msg, {'client_issuer': issuer,
'trusted_issuer': CONF.tokenless_auth.trusted_issuer}) 'trusted_issuer': CONF.tokenless_auth.trusted_issuer})
return False return False
@wsgi.middleware_exceptions
def process_request(self, request): def process_request(self, request):
resp = super(AuthContextMiddleware, self).process_request(request)
if resp:
return resp
# 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 fill_context(self, request):
# The request context stores itself in thread-local memory for logging. # The request context stores itself in thread-local memory for logging.
request_context = oslo_context.RequestContext( request_context = oslo_context.RequestContext(
request_id=request.environ.get('openstack.request_id')) request_id=request.environ.get('openstack.request_id'))
@ -198,13 +162,43 @@ class AuthContextMiddleware(wsgi.Middleware):
LOG.warning(msg) LOG.warning(msg)
return return
auth_context, token_id, is_admin = self._build_auth_context(request) # 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.
request_context.auth_token = token_id if request.environ.get(core.CONTEXT_ENV, {}).get('is_admin', False):
request_context.is_admin = is_admin request_context.is_admin = True
auth_context = {}
if auth_context is None: elif CONF.admin_token and request.user_token == CONF.admin_token:
# The client didn't send any auth info, so don't set auth context. versionutils.report_deprecated_feature(
LOG,
_LW('build_auth_context middleware checking for the admin '
'token is deprecated as of the Mitaka release and will be '
'removed in the O release. If your deployment requires '
'use of the admin token, update keystone-paste.ini so '
'that admin_token_auth is before build_auth_context in '
'the paste pipelines, otherwise remove the '
'admin_token_auth middleware from the paste pipelines.'))
request_context.is_admin = True
auth_context = {}
elif request.token_auth.has_user_token:
request_context.auth_token = request.user_token
ref = token_model.KeystoneToken(token_id=request.user_token,
token_data=request.token_info)
auth_context = authorization.token_to_auth_context(ref)
elif self._validate_trusted_issuer(request):
auth_context = self._build_tokenless_auth_context(request)
else:
LOG.debug('There is either no auth token in the request or '
'the certificate issuer is not trusted. No auth '
'context will be set.')
return return
# The attributes of request_context are put into the logs. This is a # The attributes of request_context are put into the logs. This is a
@ -220,3 +214,32 @@ class AuthContextMiddleware(wsgi.Middleware):
LOG.debug('RBAC: auth_context: %s', auth_context) LOG.debug('RBAC: auth_context: %s', auth_context)
request.environ[authorization.AUTH_CONTEXT_ENV] = 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

View File

@ -72,10 +72,10 @@ class MiddlewareRequestTestBase(unit.TestCase):
_called = False _called = False
def process_request(i_self, *i_args, **i_kwargs): def fill_context(i_self, *i_args, **i_kwargs):
# i_ to distinguish it from and not clobber the outer vars # i_ to distinguish it from and not clobber the outer vars
e = self.assertRaises(exc, e = self.assertRaises(exc,
super(_Failing, i_self).process_request, super(_Failing, i_self).fill_context,
*i_args, **i_kwargs) *i_args, **i_kwargs)
i_self._called = True i_self._called = True
raise e raise e