From be558717ed29dfe01664f68d6de42f73d3afe8ab Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 10 Dec 2015 17:04:33 +1100 Subject: [PATCH] Make AuthContext depend on auth_token middleware Reuse the validation logic that is already present in auth_token middleware. Once this is present keystone can start to reuse the same helpers that are created from auth_token middleware that the other services rely on. For now there is still some redundancy, like for example bind checking is now enforced in auth_token middleware and in keystone. These can be removed in later commits because they will require test changes. My intention after this is to start to more directly integrate this with oslo.policy and start to standardize the way auth is handled from auth_token middleware to enforcement. Doing this work here means that we get keystone to try out policy changes first. Change-Id: I6592ea2865863c9ace1304b06d73a917c3a1b114 --- keystone/common/wsgi.py | 51 +++++--- keystone/middleware/auth.py | 173 ++++++++++++++----------- keystone/tests/unit/test_middleware.py | 4 +- 3 files changed, 131 insertions(+), 97 deletions(-) diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index 04528a0c0c..53cb5631e4 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -19,6 +19,7 @@ """Utility methods for working with WSGI servers.""" import copy +import functools import itertools import re import wsgiref.util @@ -393,6 +394,30 @@ class Application(BaseApplication): 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): """Base WSGI middleware. @@ -429,27 +454,13 @@ class Middleware(Application): return response @webob.dec.wsgify() + @middleware_exceptions def __call__(self, request): - try: - response = self.process_request(request) - if response: - return response - response = request.get_response(self.application) - 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)) + response = self.process_request(request) + if response: + return response + response = request.get_response(self.application) + return self.process_response(request, response) class Debug(Middleware): diff --git a/keystone/middleware/auth.py b/keystone/middleware/auth.py index cc7d0ecc0c..a16f1d321d 100644 --- a/keystone/middleware/auth.py +++ b/keystone/middleware/auth.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from keystonemiddleware import auth_token from oslo_config import cfg from oslo_context import context as oslo_context from oslo_log import log from oslo_log import versionutils from keystone.common import authorization +from keystone.common import dependency from keystone.common import tokenless_auth from keystone.common import wsgi from keystone import exception @@ -32,83 +34,32 @@ LOG = log.getLogger(__name__) __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.""" - 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. - # 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. - - 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 + def fetch_token(self, token): + if CONF.admin_token and token == CONF.admin_token: + return {} try: - token_ref = token_model.KeystoneToken( - 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 + return self.token_provider_api.validate_token(token) except exception.TokenNotFound: - LOG.warning(_LW('RBAC: Invalid token')) - raise exception.Unauthorized() + raise auth_token.InvalidToken(_('Could not find token')) - def _build_tokenless_auth_context(self, env): + 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(env) + tokenless_helper = tokenless_auth.TokenlessAuthHelper(request.environ) (domain_id, project_id, trust_ref, unscoped) = ( tokenless_helper.get_scope()) @@ -153,7 +104,7 @@ class AuthContextMiddleware(wsgi.Middleware): in token_data['token']['roles']] return auth_context - def _validate_trusted_issuer(self, env): + def _validate_trusted_issuer(self, request): """To further filter the certificates that are trusted. 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: return False - client_issuer = env.get(CONF.tokenless_auth.issuer_attribute) - if not client_issuer: + issuer = request.environ.get(CONF.tokenless_auth.issuer_attribute) + if not issuer: msg = _LI('Cannot find client issuer in env by the ' 'issuer attribute - %s.') LOG.info(msg, CONF.tokenless_auth.issuer_attribute) return False - if client_issuer in CONF.tokenless_auth.trusted_issuer: + if issuer in CONF.tokenless_auth.trusted_issuer: return True msg = _LI('The client issuer %(client_issuer)s does not match with ' 'the trusted issuer %(trusted_issuer)s') LOG.info( - msg, {'client_issuer': client_issuer, + msg, {'client_issuer': issuer, 'trusted_issuer': CONF.tokenless_auth.trusted_issuer}) return False + @wsgi.middleware_exceptions 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. request_context = oslo_context.RequestContext( request_id=request.environ.get('openstack.request_id')) @@ -198,13 +162,43 @@ class AuthContextMiddleware(wsgi.Middleware): LOG.warning(msg) 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 - request_context.is_admin = is_admin + if request.environ.get(core.CONTEXT_ENV, {}).get('is_admin', False): + request_context.is_admin = True + auth_context = {} - if auth_context is None: - # The client didn't send any auth info, so don't set auth context. + elif CONF.admin_token and request.user_token == 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.')) + + 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 # 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) 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 diff --git a/keystone/tests/unit/test_middleware.py b/keystone/tests/unit/test_middleware.py index d33e8c001b..0fe9d3135b 100644 --- a/keystone/tests/unit/test_middleware.py +++ b/keystone/tests/unit/test_middleware.py @@ -72,10 +72,10 @@ class MiddlewareRequestTestBase(unit.TestCase): _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 e = self.assertRaises(exc, - super(_Failing, i_self).process_request, + super(_Failing, i_self).fill_context, *i_args, **i_kwargs) i_self._called = True raise e