summaryrefslogtreecommitdiff
path: root/keystonemiddleware/ec2_token.py
diff options
context:
space:
mode:
authorAndrey Pavlov <andrey-mp@yandex.ru>2015-08-03 08:19:15 +0300
committerAndrey Pavlov <andrey-mp@yandex.ru>2015-11-27 18:22:04 +0300
commit9390329f07473cd791a18e9b55c3a573872cd268 (patch)
tree6876973df998e232c31408c1172ce5dcc4fb6fb1 /keystonemiddleware/ec2_token.py
parente0e1df5ef3a96bc690975a3f611a40753aaecbd6 (diff)
downloadkeystonemiddleware-9390329f07473cd791a18e9b55c3a573872cd268.tar.gz
Adding parse of protocol v4 of AWS auth to ec2_token
This patch adds parsing of protocol v4 of AWS auth to ec2_token. This code 'copy-pasted' from nova where it works well. Also this patch adds unit tests for ec2_token middleware. Chunks of the code proposed can be seen here: https://github.com/openstack/ec2-api/blob/master/ec2api/api/__init__.py#L166 and here: https://github.com/openstack/ec2-api/blob/master/ec2api/api/faults.py We copy and paste the code since pulling in ec2api would bring in a lot of dependencies and probably create a circular one https://github.com/openstack/ec2-api/blob/master/requirements.txt Change-Id: Id03a7c78152bda35879550f2aaf94483b82f381e Closes-Bug: 1473039 Closes-Bug: 1333951
Diffstat (limited to 'keystonemiddleware/ec2_token.py')
-rw-r--r--keystonemiddleware/ec2_token.py155
1 files changed, 123 insertions, 32 deletions
diff --git a/keystonemiddleware/ec2_token.py b/keystonemiddleware/ec2_token.py
index df3bb6b..0ee97e9 100644
--- a/keystonemiddleware/ec2_token.py
+++ b/keystonemiddleware/ec2_token.py
@@ -20,11 +20,17 @@ Starting point for routing EC2 requests.
"""
+import hashlib
+import logging
+
from oslo_config import cfg
from oslo_serialization import jsonutils
import requests
+import six
import webob.dec
-import webob.exc
+
+from keystonemiddleware.i18n import _
+
keystone_ec2_opts = [
cfg.StrOpt('url',
@@ -47,45 +53,118 @@ CONF = cfg.CONF
CONF.register_opts(keystone_ec2_opts, group='keystone_ec2_token')
+PROTOCOL_NAME = 'EC2 Token Authentication'
+
+
class EC2Token(object):
"""Authenticate an EC2 request with keystone and convert to token."""
- def __init__(self, application):
+ def __init__(self, application, conf):
super(EC2Token, self).__init__()
self._application = application
+ self._logger = logging.getLogger(conf.get('log_name', __name__))
+ self._logger.debug('Starting the %s component', PROTOCOL_NAME)
+
+ def _ec2_error_response(self, code, message):
+ """Helper to construct an EC2 compatible error message."""
+ self._logger.debug('EC2 error response: %(code)s: %(message)s',
+ {'code': code, 'message': message})
+ resp = webob.Response()
+ resp.status = 400
+ resp.headers['Content-Type'] = 'text/xml'
+ error_msg = str('<?xml version="1.0"?>\n'
+ '<Response><Errors><Error><Code>%s</Code>'
+ '<Message>%s</Message></Error></Errors></Response>' %
+ (code, message))
+ if six.PY3:
+ error_msg = error_msg.encode()
+ resp.body = error_msg
+ return resp
+
+ def _get_signature(self, req):
+ """Extract the signature from the request.
+
+ This can be a get/post variable or for version 4 also in a header
+ called 'Authorization'.
+ - params['Signature'] == version 0,1,2,3
+ - params['X-Amz-Signature'] == version 4
+ - header 'Authorization' == version 4
+ """
+ sig = req.params.get('Signature') or req.params.get('X-Amz-Signature')
+ if sig is None and 'Authorization' in req.headers:
+ auth_str = req.headers['Authorization']
+ sig = auth_str.partition("Signature=")[2].split(',')[0]
+
+ return sig
+
+ def _get_access(self, req):
+ """Extract the access key identifier.
+
+ For version 0/1/2/3 this is passed as the AccessKeyId parameter, for
+ version 4 it is either an X-Amz-Credential parameter or a Credential=
+ field in the 'Authorization' header string.
+ """
+ access = req.params.get('AWSAccessKeyId')
+ if access is None:
+ cred_param = req.params.get('X-Amz-Credential')
+ if cred_param:
+ access = cred_param.split("/")[0]
+
+ if access is None and 'Authorization' in req.headers:
+ auth_str = req.headers['Authorization']
+ cred_str = auth_str.partition("Credential=")[2].split(',')[0]
+ access = cred_str.split("/")[0]
+
+ return access
@webob.dec.wsgify()
def __call__(self, req):
- # Read request signature and access id.
- try:
- signature = req.params['Signature']
- access = req.params['AWSAccessKeyId']
- except KeyError:
- raise webob.exc.HTTPBadRequest()
-
- # Make a copy of args for authentication and signature verification.
- auth_params = dict(req.params)
- # Not part of authentication args
- auth_params.pop('Signature')
-
- # Authenticate the request.
- creds = {
- 'ec2Credentials': {
- 'access': access,
- 'signature': signature,
- 'host': req.host,
- 'verb': req.method,
- 'path': req.path,
- 'params': auth_params,
- }
+ # NOTE(alevine): We need to calculate the hash here because
+ # subsequent access to request modifies the req.body so the hash
+ # calculation will yield invalid results.
+ body_hash = hashlib.sha256(req.body).hexdigest()
+
+ signature = self._get_signature(req)
+ if not signature:
+ msg = _("Signature not provided")
+ return self._ec2_error_response("AuthFailure", msg)
+ access = self._get_access(req)
+ if not access:
+ msg = _("Access key not provided")
+ return self._ec2_error_response("AuthFailure", msg)
+
+ if 'X-Amz-Signature' in req.params or 'Authorization' in req.headers:
+ auth_params = {}
+ else:
+ # Make a copy of args for authentication and signature verification
+ auth_params = dict(req.params)
+ # Not part of authentication args
+ auth_params.pop('Signature', None)
+
+ headers = req.headers
+ if six.PY3:
+ # NOTE(andrey-mp): jsonutils dumps it as list of keys without
+ # conversion instead real dict
+ headers = {k: headers[k] for k in headers}
+ cred_dict = {
+ 'access': access,
+ 'signature': signature,
+ 'host': req.host,
+ 'verb': req.method,
+ 'path': req.path,
+ 'params': auth_params,
+ 'headers': headers,
+ 'body_hash': body_hash
}
+ if "ec2" in CONF.keystone_ec2_token.url:
+ creds = {'ec2Credentials': cred_dict}
+ else:
+ creds = {'auth': {'OS-KSEC2:ec2Credentials': cred_dict}}
creds_json = jsonutils.dumps(creds)
headers = {'Content-Type': 'application/json'}
- verify = True
- if CONF.keystone_ec2_token.insecure:
- verify = False
- elif CONF.keystone_ec2_token.cafile:
+ verify = not CONF.keystone_ec2_token.insecure
+ if verify and CONF.keystone_ec2_token.cafile:
verify = CONF.keystone_ec2_token.cafile
cert = None
@@ -96,18 +175,30 @@ class EC2Token(object):
elif CONF.keystone_ec2_token.certfile:
cert = CONF.keystone_ec2_token.certfile
- response = requests.post(CONF.keystone_ec2_token.url, data=creds_json,
- headers=headers, verify=verify, cert=cert)
+ response = requests.request('POST', CONF.keystone_ec2_token.url,
+ data=creds_json, headers=headers,
+ verify=verify, cert=cert)
# NOTE(vish): We could save a call to keystone by
# having keystone return token, tenant,
# user, and roles from this call.
+ status_code = response.status_code
+ if status_code != 200:
+ msg = _('Error response from keystone: %s') % response.reason
+ self._logger.debug(msg)
+ return self._ec2_error_response("AuthFailure", msg)
result = response.json()
try:
- token_id = result['access']['token']['id']
+ if 'token' in result:
+ # NOTE(andrey-mp): response from keystone v3
+ token_id = response.headers['x-subject-token']
+ else:
+ token_id = result['access']['token']['id']
except (AttributeError, KeyError):
- raise webob.exc.HTTPBadRequest()
+ msg = _("Failure parsing response from keystone")
+ self._logger.exception(msg)
+ return self._ec2_error_response("AuthFailure", msg)
# Authenticated!
req.headers['X-Auth-Token'] = token_id