support aws v4 signature

Change-Id: Ic9af4f35239e534b4ce05cd186f071cd22f8882d
This commit is contained in:
Andrey Pavlov 2014-12-11 22:04:44 +03:00
parent efb03d20cf
commit e4c4463ab1
3 changed files with 146 additions and 81 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
*.pyc *.pyc
*~ *~
.project .project
.pydevproject .pydevproject
ec2_api.egg-info
.tox

View File

@ -15,13 +15,14 @@
""" """
Starting point for routing EC2 requests. Starting point for routing EC2 requests.
""" """
import functools
import hashlib
import sys import sys
from eventlet.green import httplib
import netaddr import netaddr
from oslo.config import cfg from oslo.config import cfg
import requests
import six import six
import six.moves.urllib.parse as urlparse
import webob import webob
import webob.dec import webob.dec
import webob.exc import webob.exc
@ -55,6 +56,14 @@ CONF.register_opts(ec2_opts)
CONF.import_opt('use_forwarded_for', 'ec2api.api.auth') CONF.import_opt('use_forwarded_for', 'ec2api.api.auth')
EMPTY_SHA256_HASH = (
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
# This is the buffer size used when calculating sha256 checksums.
# Experimenting with various buffer sizes showed that this value generally
# gave the best result (in terms of performance).
PAYLOAD_BUFFER = 1024 * 1024
# Fault Wrapper around all EC2 requests # # Fault Wrapper around all EC2 requests #
class FaultWrapper(wsgi.Middleware): class FaultWrapper(wsgi.Middleware):
@ -112,6 +121,83 @@ class EC2KeystoneAuth(wsgi.Middleware):
@webob.dec.wsgify(RequestClass=wsgi.Request) @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req): def __call__(self, req):
request_id = context.generate_request_id() request_id = context.generate_request_id()
if 'Signature' in req.params:
cred_dict = self._get_creds(req, request_id)
else:
cred_dict = self._get_creds_v4(req, request_id)
access = cred_dict['access']
token_url = CONF.keystone_url + "/ec2tokens"
if "ec2" in token_url:
creds = {'ec2Credentials': cred_dict}
else:
creds = {'auth': {'OS-KSEC2:ec2Credentials': cred_dict}}
creds_json = jsonutils.dumps(creds)
headers = {'Content-Type': 'application/json'}
response = requests.request('POST', token_url,
data=creds_json, headers=headers)
status_code = response.status_code
if status_code != 200:
if status_code == 401:
msg = response.reason
else:
msg = _("Failure communicating with keystone")
return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=status_code)
result = response.json()
try:
token_id = result['access']['token']['id']
user_id = result['access']['user']['id']
project_id = result['access']['token']['tenant']['id']
user_name = result['access']['user'].get('name')
project_name = result['access']['token']['tenant'].get('name')
roles = [role['name'] for role
in result['access']['user']['roles']]
except (AttributeError, KeyError) as e:
LOG.exception(_("Keystone failure: %s") % e)
msg = _("Failure communicating with keystone")
return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=400)
remote_address = req.remote_addr
if CONF.use_forwarded_for:
remote_address = req.headers.get('X-Forwarded-For',
remote_address)
headers["X-Auth-Token"] = token_id
url = CONF.keystone_url + ("/users/%s/credentials/OS-EC2/%s"
% (user_id, access))
response = requests.request('GET', url, headers=headers)
status_code = response.status_code
if status_code != 200:
if status_code == 401:
msg = response.reason
else:
msg = _("Failure communicating with keystone")
return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=status_code)
ec2_creds = response.json()
catalog = result['access']['serviceCatalog']
ctxt = context.RequestContext(user_id,
project_id,
ec2_creds["credential"]["access"],
ec2_creds["credential"]["secret"],
user_name=user_name,
project_name=project_name,
roles=roles,
auth_token=token_id,
remote_address=remote_address,
service_catalog=catalog,
api_version=req.params.get('Version'))
req.environ['ec2api.context'] = ctxt
return self.application
def _get_creds(self, req, request_id):
signature = req.params.get('Signature') signature = req.params.get('Signature')
if not signature: if not signature:
msg = _("Signature not provided") msg = _("Signature not provided")
@ -136,88 +222,66 @@ class EC2KeystoneAuth(wsgi.Middleware):
'path': req.path, 'path': req.path,
'params': auth_params, 'params': auth_params,
} }
token_url = CONF.keystone_url + "/ec2tokens" return cred_dict
if "ec2" in token_url:
creds = {'ec2Credentials': cred_dict}
else:
creds = {'auth': {'OS-KSEC2:ec2Credentials': cred_dict}}
creds_json = jsonutils.dumps(creds)
headers = {'Content-Type': 'application/json'}
o = urlparse.urlparse(token_url) def _get_creds_v4(self, req, request_id):
if o.scheme == "http": auth = req.environ['HTTP_AUTHORIZATION'].split(',')
conn = httplib.HTTPConnection(o.netloc) auth = [a.strip() for a in auth]
else: if not auth[0].startswith('AWS4-HMAC-SHA256'):
conn = httplib.HTTPSConnection(o.netloc) msg = _("Invalid authorization parameters")
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 faults.ec2_error_response(request_id, "AuthFailure", msg, return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=response.status) status=400)
result = jsonutils.loads(data) access = auth[0].split('=')[1].split('/')[0]
conn.close() if not access:
msg = _("Access key not provided")
try:
token_id = result['access']['token']['id']
user_id = result['access']['user']['id']
project_id = result['access']['token']['tenant']['id']
user_name = result['access']['user'].get('name')
project_name = result['access']['token']['tenant'].get('name')
roles = [role['name'] for role
in result['access']['user']['roles']]
except (AttributeError, KeyError) as e:
LOG.exception(_("Keystone failure: %s") % e)
msg = _("Failure communicating with keystone")
return faults.ec2_error_response(request_id, "AuthFailure", msg, return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=400) status=400)
remote_address = req.remote_addr for item in auth:
if CONF.use_forwarded_for: if item.startswith('Signature'):
remote_address = req.headers.get('X-Forwarded-For', signature = item.split('=')[1]
remote_address) if not signature:
msg = _("Signature could not be found in request")
headers["X-Auth-Token"] = token_id
o = urlparse.urlparse(CONF.keystone_url
+ ("/users/%s/credentials/OS-EC2/%s"
% (user_id, access)))
if o.scheme == "http":
conn = httplib.HTTPConnection(o.netloc)
else:
conn = httplib.HTTPSConnection(o.netloc)
conn.request('GET', o.path, 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 faults.ec2_error_response(request_id, "AuthFailure", msg, return faults.ec2_error_response(request_id, "AuthFailure", msg,
status=response.status) status=400)
ec2_creds = jsonutils.loads(data)
conn.close()
catalog = result['access']['serviceCatalog'] headers = dict()
ctxt = context.RequestContext(user_id, for key in req.headers:
project_id, headers[key] = req.headers.get(key)
ec2_creds["credential"]["access"],
ec2_creds["credential"]["secret"],
user_name=user_name,
project_name=project_name,
roles=roles,
auth_token=token_id,
remote_address=remote_address,
service_catalog=catalog,
api_version=req.params.get('Version'))
req.environ['ec2api.context'] = ctxt if 'X-Amz-Content-SHA256' in req.headers:
body_hash = req.headers['X-Amz-Content-SHA256']
else:
body_hash = self._payload(req)
return self.application cred_dict = {
'access': access,
'signature': signature,
'host': req.host,
'verb': req.method,
'path': req.path,
# most clients do not use req.params(that stores body for now)
'params': dict(),
'headers': headers,
'body_hash': body_hash
}
return cred_dict
def _payload(self, request):
if request.body and hasattr(request.body, 'seek'):
position = request.body.tell()
read_chunksize = functools.partial(request.body.read,
PAYLOAD_BUFFER)
checksum = hashlib.sha256()
for chunk in iter(read_chunksize, b''):
checksum.update(chunk)
hex_checksum = checksum.hexdigest()
request.body.seek(position)
return hex_checksum
elif request.body:
return hashlib.sha256(request.body.encode('utf-8')).hexdigest()
else:
return EMPTY_SHA256_HASH
class Requestify(wsgi.Middleware): class Requestify(wsgi.Middleware):
@ -242,14 +306,13 @@ class Requestify(wsgi.Middleware):
# Raise KeyError if omitted # Raise KeyError if omitted
action = req.params['Action'] action = req.params['Action']
# Fix bug lp:720157 for older (version 1) clients # Fix bug lp:720157 for older (version 1) clients
version = req.params['SignatureVersion'] version = req.params.get('SignatureVersion')
if int(version) == 1: if version and int(version) == 1:
non_args.remove('SignatureMethod') non_args.remove('SignatureMethod')
if 'SignatureMethod' in args: if 'SignatureMethod' in args:
args.pop('SignatureMethod') args.pop('SignatureMethod')
for non_arg in non_args: for non_arg in non_args:
# Remove, but raise KeyError if omitted args.pop(non_arg, None)
args.pop(non_arg)
except KeyError: except KeyError:
raise webob.exc.HTTPBadRequest() raise webob.exc.HTTPBadRequest()
except exception.InvalidRequest as err: except exception.InvalidRequest as err:

View File

@ -4,7 +4,7 @@
[composite:ec2api] [composite:ec2api]
use = egg:Paste#urlmap use = egg:Paste#urlmap
/services/Cloud: ec2apicloud /: ec2apicloud
[composite:ec2apicloud] [composite:ec2apicloud]
use = call:ec2api.api.auth:pipeline_factory use = call:ec2api.api.auth:pipeline_factory