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
This commit is contained in:
parent
dabf76f2cf
commit
be558717ed
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue