]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
heat api : Update ec2token middleware for v4 signatures
authorSteven Hardy <shardy@redhat.com>
Mon, 8 Apr 2013 13:53:04 +0000 (14:53 +0100)
committerSteven Hardy <shardy@redhat.com>
Wed, 1 May 2013 17:29:01 +0000 (18:29 +0100)
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

heat/api/aws/ec2token.py
heat/tests/test_api_ec2token.py [new file with mode: 0644]

index 1382fd1bf4de17b5362f46041008f4c9fb14683c..c907fd70bee6c734518391ddd513e92c9ae25385 100644 (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:
diff --git a/heat/tests/test_api_ec2token.py b/heat/tests/test_api_ec2token.py
new file mode 100644 (file)
index 0000000..0947bbb
--- /dev/null
@@ -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()