Merge "Make AuthContext depend on auth_token middleware"
This commit is contained in:
commit
d21edb4715
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue