Repository: boto/boto

Issue:
STSConnection : how to specify anon auth handler ?
When using assume_role_with_web_identity, the doc says : "`AssumeRoleWithWebIdentity` is an API call that does not require the use of AWS security credentials"

However, calls to STSConnection() fail with 

```
2013-06-14 09:30:08,699 boto [ERROR]:Unable to read instance data, giving up
Traceback (most recent call last):
  File "test.py", line 77, in <module>
    credentials = doGetAccessCredentials(token['id_token'], profile)
  File "test.py", line 17, in doGetAccessCredentials
    conn = boto.sts.STSConnection()
  File "/Library/Python/2.7/site-packages/boto/sts/connection.py", line 83, in __init__
    validate_certs=validate_certs)
  File "/Library/Python/2.7/site-packages/boto/connection.py", line 965, in __init__
    validate_certs=validate_certs)
  File "/Library/Python/2.7/site-packages/boto/connection.py", line 552, in __init__
    host, config, self.provider, self._required_auth_capability())
  File "/Library/Python/2.7/site-packages/boto/auth.py", line 683, in get_auth_handler
    'Check your credentials' % (len(names), str(names)))
```

This is because STSConnection uses 'sign-v2' auth handler.
How can we specify STSConnection to use anon handler instead ?



Discussion:
We do have an anonymous auth handler in boto but at the moment it's only accessible with an S3 connection by passing an `anon=True` parameter to the class constructor.

I'll try a little test to see if its possible to use that with this STS request and, if so, I'll check in a fix.

Thanks you Mitch.  How are we supposed to use the assume_role_with_web_identity then ?  By nature, this call happens when the AWS connection is not authenticated.  This is what API docs says too : "AssumeRoleWithWebIdentity is an API call that does not require the use of AWS security credentials"

Is there a way to create an unauthenticated STSConnection object ?

Currently, I switch back to plain HTTP API instead of boto and it works as expected.  I can share my code

I'm running into this also. I extended STSConnection to override the _required_auth_capability method, however now I see that AWS redirects the request to http://aws.amazon.com/iam/


Resolving patch:
diff --git a/boto/auth.py b/boto/auth.py
--- a/boto/auth.py
+++ b/boto/auth.py
@@ -512,6 +512,45 @@ def add_auth(self, req, **kwargs):
         req.headers['Authorization'] = ','.join(l)
 
 
+class QueryAuthHandler(AuthHandler):
+    """
+    Provides pure query construction (no actual signing).
+
+    Mostly useful for STS' ``assume_role_with_web_identity``.
+
+    Does **NOT** escape query string values!
+    """
+
+    capability = ['pure-query']
+
+    def _escape_value(self, value):
+        # Would normally be ``return urllib.quote(value)``.
+        return value
+
+    def _build_query_string(self, params):
+        keys = params.keys()
+        keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
+        pairs = []
+        for key in keys:
+            val = boto.utils.get_utf8_value(params[key])
+            pairs.append(key + '=' + self._escape_value(val))
+        return '&'.join(pairs)
+
+    def add_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        params = http_request.params
+        qs = self._build_query_string(
+            http_request.params
+        )
+        boto.log.debug('query_string: %s' % qs)
+        headers['Content-Type'] = 'application/json; charset=UTF-8'
+        http_request.body = ''
+        # if this is a retried request, the qs from the previous try will
+        # already be there, we need to get rid of that and rebuild it
+        http_request.path = http_request.path.split('?')[0]
+        http_request.path = http_request.path + '?' + qs
+
+
 class QuerySignatureHelper(HmacKeys):
     """
     Helper for Query signature based Auth handler.
diff --git a/boto/sts/connection.py b/boto/sts/connection.py
--- a/boto/sts/connection.py
+++ b/boto/sts/connection.py
@@ -69,12 +69,13 @@ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
                  is_secure=True, port=None, proxy=None, proxy_port=None,
                  proxy_user=None, proxy_pass=None, debug=0,
                  https_connection_factory=None, region=None, path='/',
-                 converter=None, validate_certs=True):
+                 converter=None, validate_certs=True, anon=False):
         if not region:
             region = RegionInfo(self, self.DefaultRegionName,
                                 self.DefaultRegionEndpoint,
                                 connection_cls=STSConnection)
         self.region = region
+        self.anon = anon
         self._mutex = threading.Semaphore()
         AWSQueryConnection.__init__(self, aws_access_key_id,
                                     aws_secret_access_key,
@@ -85,7 +86,10 @@ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
                                     validate_certs=validate_certs)
 
     def _required_auth_capability(self):
-        return ['sign-v2']
+        if self.anon:
+            return ['pure-query']
+        else:
+            return ['sign-v2']
 
     def _check_token_cache(self, token_key, duration=None, window_seconds=60):
         token = _session_token_cache.get(token_key, None)
@@ -437,7 +441,7 @@ def assume_role_with_web_identity(self, role_arn, role_session_name,
             'AssumeRoleWithWebIdentity',
             params,
             AssumedRole,
-            verb='POST'
+            verb='GET'
         )
 
     def decode_authorization_message(self, encoded_message):
diff --git a/boto/sts/credentials.py b/boto/sts/credentials.py
--- a/boto/sts/credentials.py
+++ b/boto/sts/credentials.py
@@ -42,6 +42,7 @@ def __init__(self, parent=None):
         self.secret_key = None
         self.session_token = None
         self.expiration = None
+        self.request_id = None
 
     @classmethod
     def from_json(cls, json_doc):
@@ -138,6 +139,7 @@ def is_expired(self, time_offset_seconds=0):
         delta = ts - now
         return delta.total_seconds() <= 0
 
+
 class FederationToken(object):
     """
     :ivar credentials: A Credentials object containing the credentials.
@@ -153,6 +155,7 @@ def __init__(self, parent=None):
         self.federated_user_arn = None
         self.federated_user_id = None
         self.packed_policy_size = None
+        self.request_id = None
 
     def startElement(self, name, attrs, connection):
         if name == 'Credentials':


Test patch:
diff --git a/tests/integration/sts/test_session_token.py b/tests/integration/sts/test_session_token.py
--- a/tests/integration/sts/test_session_token.py
+++ b/tests/integration/sts/test_session_token.py
@@ -67,13 +67,15 @@ def test_session_token(self):
         print '--- tests completed ---'
 
     def test_assume_role_with_web_identity(self):
-        c = STSConnection()
+        c = STSConnection(anon=True)
+        arn = 'arn:aws:iam::000240903217:role/FederatedWebIdentityRole'
+        wit = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
 
         try:
             creds = c.assume_role_with_web_identity(
-                'arn:aws:s3:::my_corporate_bucket/*',
-                'guestuser',
-                'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
+                role_arn=arn,
+                role_session_name='guestuser',
+                web_identity_token=wit,
                 provider_id='www.amazon.com',
             )
         except BotoServerError as err:
diff --git a/tests/unit/auth/test_query.py b/tests/unit/auth/test_query.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/auth/test_query.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2013 Amazon.com, Inc. or its affiliates.  All Rights Reserved
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+import copy
+from mock import Mock
+from tests.unit import unittest
+
+from boto.auth import QueryAuthHandler
+from boto.connection import HTTPRequest
+
+
+class TestQueryAuthHandler(unittest.TestCase):
+    def setUp(self):
+        self.provider = Mock()
+        self.provider.access_key = 'access_key'
+        self.provider.secret_key = 'secret_key'
+        self.request = HTTPRequest(
+            method='GET',
+            protocol='https',
+            host='sts.amazonaws.com',
+            port=443,
+            path='/',
+            auth_path=None,
+            params={
+                'Action': 'AssumeRoleWithWebIdentity',
+                'Version': '2011-06-15',
+                'RoleSessionName': 'web-identity-federation',
+                'ProviderId': '2012-06-01',
+                'WebIdentityToken': 'Atza|IQEBLjAsAhRkcxQ',
+            },
+            headers={},
+            body=''
+        )
+
+    def test_escape_value(self):
+        auth = QueryAuthHandler('sts.amazonaws.com',
+                                 Mock(), self.provider)
+        # This should **NOT** get escaped.
+        value = auth._escape_value('Atza|IQEBLjAsAhRkcxQ')
+        self.assertEqual(value, 'Atza|IQEBLjAsAhRkcxQ')
+
+    def test_build_query_string(self):
+        auth = QueryAuthHandler('sts.amazonaws.com',
+                                 Mock(), self.provider)
+        query_string = auth._build_query_string(self.request.params)
+        self.assertEqual(query_string, 'Action=AssumeRoleWithWebIdentity' + \
+            '&ProviderId=2012-06-01&RoleSessionName=web-identity-federation' + \
+            '&Version=2011-06-15&WebIdentityToken=Atza|IQEBLjAsAhRkcxQ')
+
+    def test_add_auth(self):
+        auth = QueryAuthHandler('sts.amazonaws.com',
+                                 Mock(), self.provider)
+        req = copy.copy(self.request)
+        auth.add_auth(req)
+        self.assertEqual(req.path,
+            '/?Action=AssumeRoleWithWebIdentity' + \
+            '&ProviderId=2012-06-01&RoleSessionName=web-identity-federation' + \
+            '&Version=2011-06-15&WebIdentityToken=Atza|IQEBLjAsAhRkcxQ')
diff --git a/tests/unit/sts/__init__.py b/tests/unit/sts/__init__.py
new file mode 100644
diff --git a/tests/unit/sts/test_connection.py b/tests/unit/sts/test_connection.py
--- a/tests/unit/sts/test_connection.py
+++ b/tests/unit/sts/test_connection.py
@@ -70,5 +70,93 @@ def test_assume_role(self):
         self.assertEqual(response.user.assume_role_id, 'roleid:myrolesession')
 
 
+class TestSTSWebIdentityConnection(AWSMockServiceTestCase):
+    connection_class = STSConnection
+
+    def setUp(self):
+        super(TestSTSWebIdentityConnection, self).setUp()
+
+    def default_body(self):
+        return """
+<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
+  <AssumeRoleWithWebIdentityResult>
+    <SubjectFromWebIdentityToken>
+      amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A
+    </SubjectFromWebIdentityToken>
+    <AssumedRoleUser>
+      <Arn>
+        arn:aws:sts::000240903217:assumed-role/FederatedWebIdentityRole/app1
+      </Arn>
+      <AssumedRoleId>
+        AROACLKWSDQRAOFQC3IDI:app1
+      </AssumedRoleId>
+    </AssumedRoleUser>
+    <Credentials>
+      <SessionToken>
+        AQoDYXdzEE0a8ANXXXXXXXXNO1ewxE5TijQyp+IPfnyowF
+      </SessionToken>
+      <SecretAccessKey>
+        secretkey
+      </SecretAccessKey>
+      <Expiration>
+        2013-05-14T23:00:23Z
+      </Expiration>
+      <AccessKeyId>
+        accesskey
+      </AccessKeyId>
+    </Credentials>
+  </AssumeRoleWithWebIdentityResult>
+  <ResponseMetadata>
+    <RequestId>ad4156e9-bce1-11e2-82e6-6b6ef249e618</RequestId>
+  </ResponseMetadata>
+</AssumeRoleWithWebIdentityResponse>
+        """
+
+    def test_assume_role_with_web_identity(self):
+        arn = 'arn:aws:iam::000240903217:role/FederatedWebIdentityRole'
+        wit = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
+
+        self.set_http_response(status_code=200)
+        response = self.service_connection.assume_role_with_web_identity(
+            role_arn=arn,
+            role_session_name='guestuser',
+            web_identity_token=wit,
+            provider_id='www.amazon.com',
+        )
+        self.assert_request_parameters({
+          'RoleSessionName': 'guestuser',
+          'AWSAccessKeyId': 'aws_access_key_id',
+          'RoleArn': arn,
+          'WebIdentityToken': wit,
+          'ProviderId': 'www.amazon.com',
+          'Action': 'AssumeRoleWithWebIdentity'
+        }, ignore_params_values=[
+          'SignatureMethod',
+          'Timestamp',
+          'SignatureVersion',
+          'Version',
+        ])
+        self.assertEqual(
+          response.credentials.access_key.strip(),
+          'accesskey'
+        )
+        self.assertEqual(
+          response.credentials.secret_key.strip(),
+          'secretkey'
+        )
+        self.assertEqual(
+          response.credentials.session_token.strip(),
+          'AQoDYXdzEE0a8ANXXXXXXXXNO1ewxE5TijQyp+IPfnyowF'
+        )
+        self.assertEqual(
+          response.user.arn.strip(),
+          'arn:aws:sts::000240903217:assumed-role/FederatedWebIdentityRole/app1'
+        )
+        self.assertEqual(
+          response.user.assume_role_id.strip(),
+          'AROACLKWSDQRAOFQC3IDI:app1'
+        )
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/sts/test_credentials.py b/tests/unit/sts/test_credentials.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/sts/test_credentials.py
@@ -0,0 +1,38 @@
+import unittest
+
+from boto.sts.credentials import Credentials
+
+
+class STSCredentialsTest(unittest.TestCase):
+    sts = True
+
+    def setUp(self):
+        super(STSCredentialsTest, self).setUp()
+        self.creds = Credentials()
+
+    def test_to_dict(self):
+        # This would fail miserably if ``Credentials.request_id`` hadn't been
+        # explicitly set (no default).
+        # Default.
+        self.assertEqual(self.creds.to_dict(), {
+            'access_key': None,
+            'expiration': None,
+            'request_id': None,
+            'secret_key': None,
+            'session_token': None
+        })
+
+        # Override.
+        creds = Credentials()
+        creds.access_key = 'something'
+        creds.secret_key = 'crypto'
+        creds.session_token = 'this'
+        creds.expiration = 'way'
+        creds.request_id = 'comes'
+        self.assertEqual(creds.to_dict(), {
+            'access_key': 'something',
+            'expiration': 'way',
+            'request_id': 'comes',
+            'secret_key': 'crypto',
+            'session_token': 'this'
+        })