Optimizes ec2 keystone usage and handles errors
* breaks out gen_request_id so we can return it in error msg * breaks out ec2_error so we can use it in multiple middlewares * adds new middleware (remove old after devstack change) * skips extra call to keystone for second authentication * fixes bug 922373 Change-Id: If765d149289255b0bf0e0c1b647ebb547ce5759b
This commit is contained in:
@@ -39,7 +39,7 @@ pipeline = ec2faultwrap logrequest ec2noauth cloudrequest authorizer validator e
|
||||
# NOTE(vish): use the following pipeline for deprecated auth
|
||||
# pipeline = ec2faultwrap logrequest authenticate cloudrequest authorizer validator ec2executor
|
||||
# NOTE(vish): use the following pipeline for keystone auth
|
||||
# pipeline = ec2faultwrap logrequest totoken authtoken keystonecontext cloudrequest authorizer validator ec2executor
|
||||
# pipeline = ec2faultwrap logrequest ec2keystoneauth cloudrequest authorizer validator ec2executor
|
||||
|
||||
[filter:ec2faultwrap]
|
||||
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||
@@ -53,6 +53,9 @@ paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||
[filter:totoken]
|
||||
paste.filter_factory = nova.api.ec2:EC2Token.factory
|
||||
|
||||
[filter:ec2keystoneauth]
|
||||
paste.filter_factory = nova.api.ec2:EC2KeystoneAuth.factory
|
||||
|
||||
[filter:ec2noauth]
|
||||
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ class NovaKeystoneContext(wsgi.Middleware):
|
||||
req.headers.get('X_STORAGE_TOKEN'))
|
||||
|
||||
# Build a context, including the auth_token...
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
remote_address = req.remote_addr
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
|
||||
+110
-30
@@ -64,6 +64,21 @@ FLAGS.add_options(ec2_opts)
|
||||
flags.DECLARE('use_forwarded_for', 'nova.api.auth')
|
||||
|
||||
|
||||
def ec2_error(req, request_id, code, message):
|
||||
"""Helper to send an ec2_compatible error"""
|
||||
LOG.error(_('%(code)s: %(message)s') % locals())
|
||||
resp = webob.Response()
|
||||
resp.status = 400
|
||||
resp.headers['Content-Type'] = 'text/xml'
|
||||
resp.body = str('<?xml version="1.0"?>\n'
|
||||
'<Response><Errors><Error><Code>%s</Code>'
|
||||
'<Message>%s</Message></Error></Errors>'
|
||||
'<RequestID>%s</RequestID></Response>' %
|
||||
(utils.utf8(code), utils.utf8(message),
|
||||
utils.utf8(request_id)))
|
||||
return resp
|
||||
|
||||
|
||||
## Fault Wrapper around all EC2 requests ##
|
||||
class FaultWrapper(wsgi.Middleware):
|
||||
"""Calls the middleware stack, captures any exceptions into faults."""
|
||||
@@ -168,7 +183,7 @@ class Lockout(wsgi.Middleware):
|
||||
|
||||
|
||||
class EC2Token(wsgi.Middleware):
|
||||
"""Authenticate an EC2 request with keystone and convert to token."""
|
||||
"""Deprecated, only here to make merging easier."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
@@ -237,6 +252,85 @@ class EC2Token(wsgi.Middleware):
|
||||
return self.application
|
||||
|
||||
|
||||
class EC2KeystoneAuth(wsgi.Middleware):
|
||||
"""Authenticate an EC2 request with keystone and convert to context."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
request_id = context.generate_request_id()
|
||||
signature = req.params.get('Signature')
|
||||
if not signature:
|
||||
msg = _("Signature not provided")
|
||||
return ec2_error(req, request_id, "Unauthorized", msg)
|
||||
access = req.params.get('AWSAccessKeyId')
|
||||
if not access:
|
||||
msg = _("Access key not provided")
|
||||
return ec2_error(req, request_id, "Unauthorized", msg)
|
||||
|
||||
# Make a copy of args for authentication and signature verification.
|
||||
auth_params = dict(req.params)
|
||||
# Not part of authentication args
|
||||
auth_params.pop('Signature')
|
||||
|
||||
cred_dict = {
|
||||
'access': access,
|
||||
'signature': signature,
|
||||
'host': req.host,
|
||||
'verb': req.method,
|
||||
'path': req.path,
|
||||
'params': auth_params,
|
||||
}
|
||||
if "ec2" in FLAGS.keystone_ec2_url:
|
||||
creds = {'ec2Credentials': cred_dict}
|
||||
else:
|
||||
creds = {'auth': {'OS-KSEC2:ec2Credentials': cred_dict}}
|
||||
creds_json = utils.dumps(creds)
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
o = urlparse.urlparse(FLAGS.keystone_ec2_url)
|
||||
if o.scheme == "http":
|
||||
conn = httplib.HTTPConnection(o.netloc)
|
||||
else:
|
||||
conn = httplib.HTTPSConnection(o.netloc)
|
||||
conn.request('POST', o.path, body=creds_json, headers=headers)
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
if response.status != 200:
|
||||
if response.status == 401:
|
||||
msg = response.reason
|
||||
else:
|
||||
msg = _("Failure communicating with keystone")
|
||||
return ec2_error(req, request_id, "Unauthorized", msg)
|
||||
result = utils.loads(data)
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
token_id = result['access']['token']['id']
|
||||
user_id = result['access']['user']['id']
|
||||
project_id = result['access']['token']['tenant']
|
||||
roles = [role['name'] for role
|
||||
in result['access']['user']['roles']]
|
||||
except (AttributeError, KeyError), e:
|
||||
LOG.exception("Keystone failure: %s" % e)
|
||||
msg = _("Failure communicating with keystone")
|
||||
return ec2_error(req, request_id, "Unauthorized", msg)
|
||||
|
||||
remote_address = req.remote_addr
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For',
|
||||
remote_address)
|
||||
ctxt = context.RequestContext(user_id,
|
||||
project_id,
|
||||
roles=roles,
|
||||
auth_token=token_id,
|
||||
strategy='keystone',
|
||||
remote_address=remote_address)
|
||||
|
||||
req.environ['nova.context'] = ctxt
|
||||
|
||||
return self.application
|
||||
|
||||
|
||||
class NoAuth(wsgi.Middleware):
|
||||
"""Add user:project as 'nova.context' to WSGI environ."""
|
||||
|
||||
@@ -246,7 +340,7 @@ class NoAuth(wsgi.Middleware):
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
user_id, _sep, project_id = req.params['AWSAccessKeyId'].partition(':')
|
||||
project_id = project_id or user_id
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
remote_address = req.remote_addr
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
@@ -478,6 +572,7 @@ class Executor(wsgi.Application):
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
context = req.environ['nova.context']
|
||||
request_id = context.request_id
|
||||
api_request = req.environ['ec2.request']
|
||||
result = None
|
||||
try:
|
||||
@@ -487,58 +582,56 @@ class Executor(wsgi.Application):
|
||||
context=context)
|
||||
ec2_id = ec2utils.id_to_ec2_id(ex.kwargs['instance_id'])
|
||||
message = ex.message % {'instance_id': ec2_id}
|
||||
return self._error(req, context, type(ex).__name__, message)
|
||||
return ec2_error(req, request_id, type(ex).__name__, message)
|
||||
except exception.VolumeNotFound as ex:
|
||||
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
ec2_id = ec2utils.id_to_ec2_vol_id(ex.kwargs['volume_id'])
|
||||
message = ex.message % {'volume_id': ec2_id}
|
||||
return self._error(req, context, type(ex).__name__, message)
|
||||
return ec2_error(req, request_id, type(ex).__name__, message)
|
||||
except exception.SnapshotNotFound as ex:
|
||||
LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
ec2_id = ec2utils.id_to_ec2_snap_id(ex.kwargs['snapshot_id'])
|
||||
message = ex.message % {'snapshot_id': ec2_id}
|
||||
return self._error(req, context, type(ex).__name__, message)
|
||||
return ec2_error(req, request_id, type(ex).__name__, message)
|
||||
except exception.NotFound as ex:
|
||||
LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except exception.ApiError as ex:
|
||||
LOG.exception(_('ApiError raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
if ex.code:
|
||||
return self._error(req, context, ex.code, unicode(ex))
|
||||
return ec2_error(req, request_id, ex.code, unicode(ex))
|
||||
else:
|
||||
return self._error(req, context, type(ex).__name__,
|
||||
return ec2_error(req, request_id, type(ex).__name__,
|
||||
unicode(ex))
|
||||
except exception.KeyPairExists as ex:
|
||||
LOG.debug(_('KeyPairExists raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except exception.InvalidParameterValue as ex:
|
||||
LOG.debug(_('InvalidParameterValue raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except exception.InvalidPortRange as ex:
|
||||
LOG.debug(_('InvalidPortRange raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except exception.NotAuthorized as ex:
|
||||
LOG.info(_('NotAuthorized raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except exception.InvalidRequest as ex:
|
||||
LOG.debug(_('InvalidRequest raised: %s'), unicode(ex),
|
||||
context=context)
|
||||
return self._error(req, context, type(ex).__name__, unicode(ex))
|
||||
return ec2_error(req, request_id, type(ex).__name__, unicode(ex))
|
||||
except Exception as ex:
|
||||
extra = {'environment': req.environ}
|
||||
LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
|
||||
extra=extra, context=context)
|
||||
return self._error(req,
|
||||
context,
|
||||
'UnknownError',
|
||||
_('An unknown error has occurred. '
|
||||
return ec2_error(req, request_id, 'UnknownError',
|
||||
_('An unknown error has occurred. '
|
||||
'Please try your request again.'))
|
||||
else:
|
||||
resp = webob.Response()
|
||||
@@ -546,16 +639,3 @@ class Executor(wsgi.Application):
|
||||
resp.headers['Content-Type'] = 'text/xml'
|
||||
resp.body = str(result)
|
||||
return resp
|
||||
|
||||
def _error(self, req, context, code, message):
|
||||
LOG.error(_('%(code)s: %(message)s') % locals())
|
||||
resp = webob.Response()
|
||||
resp.status = 400
|
||||
resp.headers['Content-Type'] = 'text/xml'
|
||||
resp.body = str('<?xml version="1.0"?>\n'
|
||||
'<Response><Errors><Error><Code>%s</Code>'
|
||||
'<Message>%s</Message></Error></Errors>'
|
||||
'<RequestID>%s</RequestID></Response>' %
|
||||
(utils.utf8(code), utils.utf8(message),
|
||||
utils.utf8(context.request_id)))
|
||||
return resp
|
||||
|
||||
+5
-2
@@ -20,12 +20,15 @@
|
||||
"""RequestContext: context for requests that persist through all of nova."""
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
|
||||
from nova import local
|
||||
from nova import utils
|
||||
|
||||
|
||||
def generate_request_id():
|
||||
return 'req-' + str(utils.gen_uuid())
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""Security context and request information.
|
||||
|
||||
@@ -59,7 +62,7 @@ class RequestContext(object):
|
||||
timestamp = utils.parse_strtime(timestamp)
|
||||
self.timestamp = timestamp
|
||||
if not request_id:
|
||||
request_id = 'req-' + str(utils.gen_uuid())
|
||||
request_id = generate_request_id()
|
||||
self.request_id = request_id
|
||||
self.auth_token = auth_token
|
||||
self.strategy = strategy
|
||||
|
||||
Reference in New Issue
Block a user