Repository: apache/libcloud

Issue:
SignatureDoesNotMatch for manual request
## Summary
While attempting to perform a manual request, it appears to be an issue with the credentials being used.


## Detailed Information

Upon attempting to create EC2 Launch Template, by performing a manual request with the EC2 driver, it's returning `libcloud.common.types.InvalidCredsError: 'SignatureDoesNotMatch`

More specifically I have a function which I attempt to create a simple Launch Template
```
def initialize_test():

    # authenticate
    aws = get_driver(Provider.EC2)
    driver = login_aws(aws)

    // Other actions that are used e.g. driver.ex_create_security_group works perfect
    // Manually performing requests isn't working
    new_template(driver)
    
def new_template(driver):
    params = {
        'Action': 'CreateLaunchTemplate',
        'LaunchTemplateName': "test-template",
        'LaunchTemplateData': {
            'ImageId': "ami-06b41651a26fbba09",
            'InstanceType': "t3.xlarge",
            'NetworkInterface': {
                1: {
                    'AssociatePublicIpAddress': True
                }
            }
        }
    }

    // Breaks here
    response = driver.connection.request("/", params=params)
```


And this throws
```
libcloud.common.types.InvalidCredsError: 'SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.'
```
The project used is running python 3.7.4, using apache-libcloud 2.5.0.



Discussion:
Which signature algorithm you are using? I believe we default to v2.

I imagine it's something related to the nested dictionary you are trying to use - in EC2 driver we send data via query parameters and not as part of a request body.

This means you should use ``parent key.child key``` notation for your query param keys. So something like "params = {"LaunchTemplateData.ImageId: "...."}``. You can find an example of that here - https://github.com/apache/libcloud/pull/1224/files#diff-36825a12922cd525dde5f5dd9635f775R1887

If that doesn't work, I would add some debug / print statements to this method and see what is going on - https://github.com/apache/libcloud/blob/trunk/libcloud/common/aws.py#L205

And perhaps just try:

```python
    params = {
        'Action': 'CreateLaunchTemplate',
    }
```
 To see if that works (I believe it should and shouldn't throw signature mismatch error).
@x-wf^ did you have a chance yet to test it out?
Hi @Kami ,

Thank you for getting back at me.
The solution was to exactly as you stated `"params = {"LaunchTemplateData.ImageId: "...."}"`. But errors like these could somehow be a bit more clear.

Once again, thank you.
I agree, we should throw in such scenario.

I will look into a code change for that.

Resolving patch:
diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py
--- a/libcloud/common/aws.py
+++ b/libcloud/common/aws.py
@@ -26,6 +26,8 @@
     import json
 
 from libcloud.utils.py3 import ET
+from libcloud.utils.py3 import _real_unicode
+from libcloud.utils.py3 import basestring
 from libcloud.common.base import ConnectionUserAndKey, XmlResponse, BaseDriver
 from libcloud.common.base import JsonResponse
 from libcloud.common.types import InvalidCredsError, MalformedResponseError
@@ -48,6 +50,23 @@
 DEFAULT_SIGNATURE_VERSION = '2'
 UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'
 
+PARAMS_NOT_STRING_ERROR_MSG = """
+"params" dictionary contains an attribute "%s" which value (%s, %s) is not a
+string.
+
+Parameters are sent via query parameters and not via request body and as such,
+all the values need to be of a simple type (string, int, bool).
+
+For arrays and other complex types, you should use notation similar to this
+one:
+
+params['TagSpecification.1.Tag.Value'] = 'foo'
+params['TagSpecification.2.Tag.Value'] = 'bar'
+
+See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
+for details.
+""".strip()
+
 
 class AWSBaseResponse(XmlResponse):
     namespace = None
@@ -378,6 +397,15 @@ def add_default_params(self, params):
         params = self.signer.get_request_params(params=params,
                                                 method=self.method,
                                                 path=self.action)
+
+        # Verify that params only contain simple types and no nested
+        # dictionaries.
+        # params are sent via query params so only strings are supported
+        for key, value in params.items():
+            if not isinstance(value, (_real_unicode, basestring, int, bool)):
+                msg = PARAMS_NOT_STRING_ERROR_MSG % (key, value, type(value))
+                raise ValueError(msg)
+
         return params
 
     def pre_connect_hook(self, params, headers):
diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py
--- a/libcloud/compute/drivers/ec2.py
+++ b/libcloud/compute/drivers/ec2.py
@@ -3866,7 +3866,8 @@ def ex_change_node_size(self, node, new_size):
                                  'the current one')
 
         attributes = {'InstanceType.Value': new_size.id}
-        return self.ex_modify_instance_attribute(node, attributes)
+        return self.ex_modify_instance_attribute(node=node,
+                                                 attributes=attributes)
 
     def ex_start_node(self, node):
         """
@@ -6023,7 +6024,7 @@ def ex_create_network(self, cidr_block, name=None):
     def ex_modify_instance_attribute(self, node, disable_api_termination=None,
                                      ebs_optimized=None, group_id=None,
                                      source_dest_check=None, user_data=None,
-                                     instance_type=None):
+                                     instance_type=None, attributes=None):
         """
         Modifies node attributes.
         Ouscale supports the following attributes:
@@ -6040,7 +6041,7 @@ def ex_modify_instance_attribute(self, node, disable_api_termination=None,
         :return: True on success, False otherwise.
         :rtype: ``bool``
         """
-        attributes = {}
+        attributes = attributes or {}
 
         if disable_api_termination is not None:
             attributes['DisableApiTermination.Value'] = disable_api_termination


Test patch:
diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py
--- a/libcloud/test/compute/test_ec2.py
+++ b/libcloud/test/compute/test_ec2.py
@@ -1313,6 +1313,23 @@ def test_ex_describe_volumes_modifications(self):
         self.assertEqual('io1', modifications[1].target_volume_type)
         self.assertEqual('vol-bEXAMPLE', modifications[1].volume_id)
 
+    def test_params_is_not_simple_type_exception_is_thrown(self):
+        params = {
+            'not': {'not': ['simple']}
+        }
+
+        expected_msg = 'dictionary contains an attribute "not" which value'
+        self.assertRaisesRegexp(ValueError, expected_msg,
+                               self.driver.connection.request, '/', params=params)
+
+        params = {
+            'invalid': [1, 2, 3]
+        }
+
+        expected_msg = 'dictionary contains an attribute "invalid" which value'
+        self.assertRaisesRegexp(ValueError, expected_msg,
+                               self.driver.connection.request, '/', params=params)
+
 
 class EC2USWest1Tests(EC2Tests):
     region = 'us-west-1'