diff options
author | Andrey Pavlov <andrey-mp@yandex.ru> | 2015-08-03 08:19:15 +0300 |
---|---|---|
committer | Andrey Pavlov <andrey-mp@yandex.ru> | 2015-11-27 18:22:04 +0300 |
commit | 9390329f07473cd791a18e9b55c3a573872cd268 (patch) | |
tree | 6876973df998e232c31408c1172ce5dcc4fb6fb1 /keystonemiddleware/ec2_token.py | |
parent | e0e1df5ef3a96bc690975a3f611a40753aaecbd6 (diff) | |
download | keystonemiddleware-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.py | 155 |
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 |