heat api : Update ec2token middleware for v4 signatures

Update ec2token so it can verify v4 signature formats.

Note for v4 signatures to work you currently need to install
the latest python-keystoneclient, as no release yet contains
patch ref https://review.openstack.org/#/c/26013/

This change should be backwards compatible, as older keystoneclient
versions will simply ignore the additional request keys

fixes bug #1122472

Change-Id: Iccc6be7913ab5ca5813a2e0c8f66cda0ccd85a0b
This commit is contained in:
Steven Hardy 2013-04-08 14:53:04 +01:00
parent a0df5125f4
commit 8518ca0558
2 changed files with 370 additions and 10 deletions

View File

@ -16,6 +16,7 @@
import urlparse
import httplib
import gettext
import hashlib
gettext.install('heat', unicode=1)
@ -52,6 +53,43 @@ class EC2Token(wsgi.Middleware):
else:
return cfg.CONF.ec2authtoken[name]
def _get_signature(self, req):
"""
Extract the signature from the request, this can be a get/post
variable or for v4 also in a header called 'Authorization'
- params['Signature'] == version 0,1,2,3
- params['X-Amz-Signature'] == version 4
- header 'Authorization' == version 4
see http://docs.aws.amazon.com/general/latest/gr/
sigv4-signed-request-examples.html
"""
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 v 0/1/2/3 this is passed
as the AccessKeyId parameter, for version4 it is either and
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(RequestClass=wsgi.Request)
def __call__(self, req):
# Read request signature and access id.
@ -60,37 +98,40 @@ class EC2Token(wsgi.Middleware):
# Returning here just means the user didn't supply AWS
# authentication and we'll let the app try native keystone next.
logger.info("Checking AWS credentials..")
try:
signature = req.params['Signature']
except KeyError:
logger.info("No AWS Signature found.")
signature = self._get_signature(req)
if not signature:
if 'X-Auth-User' in req.headers:
return self.application
else:
logger.info("No AWS Signature found.")
raise exception.HeatIncompleteSignatureError()
try:
access = req.params['AWSAccessKeyId']
except KeyError:
logger.info("No AWSAccessKeyId found.")
access = self._get_access(req)
if not access:
if 'X-Auth-User' in req.headers:
return self.application
else:
logger.info("No AWSAccessKeyId/Authorization Credential")
raise exception.HeatMissingAuthenticationTokenError()
logger.info("AWS credentials found, checking against keystone.")
# Make a copy of args for authentication and signature verification.
auth_params = dict(req.params)
# Not part of authentication args
auth_params.pop('Signature')
# 'Signature' param Not part of authentication args
auth_params.pop('Signature', None)
# Authenticate the request.
# AWS v4 authentication requires a hash of the body
body_hash = hashlib.sha256(req.body).hexdigest()
creds = {'ec2Credentials': {'access': access,
'signature': signature,
'host': req.host,
'verb': req.method,
'path': req.path,
'params': auth_params,
'headers': req.headers,
'body_hash': body_hash
}}
creds_json = None
try:

View File

@ -0,0 +1,319 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from heat.tests.common import HeatTestCase
import mox
import httplib
import json
from oslo.config import cfg
from heat.api.aws import exception
from heat.common.wsgi import Request
from heat.api.aws import ec2token
class AWSCommon(HeatTestCase):
'''
Tests the Ec2Token middleware
'''
def _dummy_GET_request(self, params={}, environ={}):
# Mangle the params dict into a query string
qs = "&".join(["=".join([k, str(params[k])]) for k in params])
environ.update({'REQUEST_METHOD': 'GET', 'QUERY_STRING': qs})
req = Request(environ)
return req
def test_conf_get_paste(self):
dummy_conf = {'auth_uri': 'abc',
'keystone_ec2_uri': 'xyz'}
ec2 = ec2token.EC2Token(app=None, conf=dummy_conf)
self.assertEqual(ec2._conf_get('auth_uri'), 'abc')
self.assertEqual(ec2._conf_get('keystone_ec2_uri'), 'xyz')
def test_conf_get_opts(self):
cfg.CONF.set_default('auth_uri', 'abc', group='ec2authtoken')
cfg.CONF.set_default('keystone_ec2_uri', 'xyz', group='ec2authtoken')
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._conf_get('auth_uri'), 'abc')
self.assertEqual(ec2._conf_get('keystone_ec2_uri'), 'xyz')
def test_get_signature_param_old(self):
params = {'Signature': 'foo'}
dummy_req = self._dummy_GET_request(params)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_signature(dummy_req), 'foo')
def test_get_signature_param_new(self):
params = {'X-Amz-Signature': 'foo'}
dummy_req = self._dummy_GET_request(params)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_signature(dummy_req), 'foo')
def test_get_signature_header_space(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_signature(dummy_req), 'xyz')
def test_get_signature_header_notlast(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar, '
'Signature=xyz,'
'SignedHeaders=content-type;host;x-amz-date ')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_signature(dummy_req), 'xyz')
def test_get_signature_header_nospace(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar,'
'SignedHeaders=content-type;host;x-amz-date,'
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_signature(dummy_req), 'xyz')
def test_get_access_param_old(self):
params = {'AWSAccessKeyId': 'foo'}
dummy_req = self._dummy_GET_request(params)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_access(dummy_req), 'foo')
def test_get_access_param_new(self):
params = {'X-Amz-Credential': 'foo/bar'}
dummy_req = self._dummy_GET_request(params)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_access(dummy_req), 'foo')
def test_get_access_header_space(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_access(dummy_req), 'foo')
def test_get_access_header_nospace(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar,'
'SignedHeaders=content-type;host;x-amz-date,'
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_access(dummy_req), 'foo')
def test_get_access_header_last(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo '
'SignedHeaders=content-type;host;x-amz-date,'
'Signature=xyz,Credential=foo/bar')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app=None, conf={})
self.assertEqual(ec2._get_access(dummy_req), 'foo')
def test_call_x_auth_user(self):
req_env = {'HTTP_X_AUTH_USER': 'foo'}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app='xyz', conf={})
self.assertEqual(ec2.__call__(dummy_req), 'xyz')
def test_call_auth_nosig(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app='xyz', conf={})
self.assertRaises(exception.HeatIncompleteSignatureError,
ec2.__call__, dummy_req)
def test_call_auth_nouser(self):
req_env = {'HTTP_AUTHORIZATION':
('Authorization: foo '
'SignedHeaders=content-type;host;x-amz-date,'
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app='xyz', conf={})
self.assertRaises(exception.HeatMissingAuthenticationTokenError,
ec2.__call__, dummy_req)
def test_call_auth_noaccess(self):
# If there's no accesskey in params or header, but there is a
# Signature, we expect HeatMissingAuthenticationTokenError
params = {'Signature': 'foo'}
dummy_req = self._dummy_GET_request(params)
ec2 = ec2token.EC2Token(app='xyz', conf={})
self.assertRaises(exception.HeatMissingAuthenticationTokenError,
ec2.__call__, dummy_req)
def test_call_x_auth_nouser_x_auth_user(self):
req_env = {'HTTP_X_AUTH_USER': 'foo',
'HTTP_AUTHORIZATION':
('Authorization: foo '
'SignedHeaders=content-type;host;x-amz-date,'
'Signature=xyz')}
dummy_req = self._dummy_GET_request(environ=req_env)
ec2 = ec2token.EC2Token(app='xyz', conf={})
self.assertEqual(ec2.__call__(dummy_req), 'xyz')
def _stub_http_connection(self, headers={}, params={}, response=None):
class DummyHTTPResponse:
resp = response
def read(self):
return self.resp
self.m.StubOutWithMock(httplib.HTTPConnection, '__init__')
httplib.HTTPConnection.__init__(mox.IgnoreArg()).AndReturn(None)
self.m.StubOutWithMock(httplib.HTTPConnection, 'request')
body_hash = ('e3b0c44298fc1c149afbf4c8996fb9'
'2427ae41e4649b934ca495991b7852b855')
req_creds = json.dumps({"ec2Credentials":
{"access": "foo",
"headers": headers,
"host": "heat:8000",
"verb": "GET",
"params": params,
"signature": "xyz",
"path": "/v1",
"body_hash": body_hash}})
req_headers = {'Content-Type': 'application/json'}
req_path = '/foo'
httplib.HTTPConnection.request('POST', req_path,
body=req_creds,
headers=req_headers).AndReturn(None)
self.m.StubOutWithMock(httplib.HTTPConnection, 'getresponse')
httplib.HTTPConnection.getresponse().AndReturn(DummyHTTPResponse())
self.m.StubOutWithMock(httplib.HTTPConnection, 'close')
httplib.HTTPConnection.close().AndReturn(None)
def test_call_ok(self):
dummy_conf = {'auth_uri': 'http://123:5000/foo',
'keystone_ec2_uri': 'http://456:5000/foo'}
ec2 = ec2token.EC2Token(app='woot', conf=dummy_conf)
auth_str = ('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')
req_env = {'SERVER_NAME': 'heat',
'SERVER_PORT': '8000',
'PATH_INFO': '/v1',
'HTTP_AUTHORIZATION': auth_str}
dummy_req = self._dummy_GET_request(environ=req_env)
ok_resp = json.dumps({'access': {'token': {'id': 123}}})
self._stub_http_connection(headers={'Authorization': auth_str},
response=ok_resp)
self.m.ReplayAll()
self.assertEqual(ec2.__call__(dummy_req), 'woot')
self.m.VerifyAll()
def test_call_err_tokenid(self):
dummy_conf = {'auth_uri': 'http://123:5000/foo',
'keystone_ec2_uri': 'http://456:5000/foo'}
ec2 = ec2token.EC2Token(app='woot', conf=dummy_conf)
auth_str = ('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')
req_env = {'SERVER_NAME': 'heat',
'SERVER_PORT': '8000',
'PATH_INFO': '/v1',
'HTTP_AUTHORIZATION': auth_str}
dummy_req = self._dummy_GET_request(environ=req_env)
err_msg = "EC2 access key not found."
err_resp = json.dumps({'error': {'message': err_msg}})
self._stub_http_connection(headers={'Authorization': auth_str},
response=err_resp)
self.m.ReplayAll()
self.assertRaises(exception.HeatInvalidClientTokenIdError,
ec2.__call__, dummy_req)
self.m.VerifyAll()
def test_call_err_signature(self):
dummy_conf = {'auth_uri': 'http://123:5000/foo',
'keystone_ec2_uri': 'http://456:5000/foo'}
ec2 = ec2token.EC2Token(app='woot', conf=dummy_conf)
auth_str = ('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')
req_env = {'SERVER_NAME': 'heat',
'SERVER_PORT': '8000',
'PATH_INFO': '/v1',
'HTTP_AUTHORIZATION': auth_str}
dummy_req = self._dummy_GET_request(environ=req_env)
err_msg = "EC2 signature not supplied."
err_resp = json.dumps({'error': {'message': err_msg}})
self._stub_http_connection(headers={'Authorization': auth_str},
response=err_resp)
self.m.ReplayAll()
self.assertRaises(exception.HeatSignatureError,
ec2.__call__, dummy_req)
self.m.VerifyAll()
def test_call_err_denied(self):
dummy_conf = {'auth_uri': 'http://123:5000/foo',
'keystone_ec2_uri': 'http://456:5000/foo'}
ec2 = ec2token.EC2Token(app='woot', conf=dummy_conf)
auth_str = ('Authorization: foo Credential=foo/bar, '
'SignedHeaders=content-type;host;x-amz-date, '
'Signature=xyz')
req_env = {'SERVER_NAME': 'heat',
'SERVER_PORT': '8000',
'PATH_INFO': '/v1',
'HTTP_AUTHORIZATION': auth_str}
dummy_req = self._dummy_GET_request(environ=req_env)
err_resp = json.dumps({})
self._stub_http_connection(headers={'Authorization': auth_str},
response=err_resp)
self.m.ReplayAll()
self.assertRaises(exception.HeatAccessDeniedError,
ec2.__call__, dummy_req)
self.m.VerifyAll()
def test_call_ok_v2(self):
dummy_conf = {'auth_uri': 'http://123:5000/foo',
'keystone_ec2_uri': 'http://456:5000/foo'}
ec2 = ec2token.EC2Token(app='woot', conf=dummy_conf)
params = {'AWSAccessKeyId': 'foo', 'Signature': 'xyz'}
req_env = {'SERVER_NAME': 'heat',
'SERVER_PORT': '8000',
'PATH_INFO': '/v1'}
dummy_req = self._dummy_GET_request(params, req_env)
ok_resp = json.dumps({'access': {'token': {'id': 123}}})
self._stub_http_connection(response=ok_resp,
params={'AWSAccessKeyId': 'foo'})
self.m.ReplayAll()
self.assertEqual(ec2.__call__(dummy_req), 'woot')
self.m.VerifyAll()