Skip to content

Commit

Permalink
Optimizes ec2 keystone usage and handles errors
Browse files Browse the repository at this point in the history
 * 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
  • Loading branch information
vishvananda committed Feb 8, 2012
1 parent b0a708f commit 13b82db
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 34 deletions.
5 changes: 4 additions & 1 deletion etc/nova/api-paste.ini
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 0 additions & 1 deletion nova/api/auth.py
Expand Up @@ -75,7 +75,6 @@ def __call__(self, req):
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)
Expand Down
140 changes: 110 additions & 30 deletions nova/api/ec2/__init__.py
Expand Up @@ -64,6 +64,21 @@
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."""
Expand Down Expand Up @@ -168,7 +183,7 @@ def __call__(self, req):


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):
Expand Down Expand Up @@ -237,6 +252,85 @@ def __call__(self, req):
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."""

Expand All @@ -246,7 +340,7 @@ def __call__(self, req):
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,
Expand Down Expand Up @@ -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:
Expand All @@ -487,75 +582,60 @@ def __call__(self, req):
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()
resp.status = 200
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
7 changes: 5 additions & 2 deletions nova/context.py
Expand Up @@ -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.
Expand Down Expand Up @@ -59,7 +62,7 @@ def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
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
Expand Down

0 comments on commit 13b82db

Please sign in to comment.