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:
parent
a0df5125f4
commit
8518ca0558
|
@ -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:
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue