Repository: boto/botocore
Pull request #964 (2016-06-23 00:03:47)

Add choose-signer event to presigners
This adds the choose-signer event to the presigers, allowing
for hot swapping the signature version when presigning.
Additionally, adds support for using botocore.UNSIGNED with the
presigners.

cc @kyleknap @jamesls

codecov-io: ## [Current coverage][cc-pull] is **97.32%**
> Merging [#964][cc-pull] into [develop][cc-base-branch] will increase coverage by **<.01%**

```diff
@@            develop       #964   diff @@
==========================================
  Files            43         43          
  Lines          7014       7033    +19   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           6826       6845    +19   
  Misses          188        188          
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last updated by [5ede912...97f1a32][cc-compare]
[cc-base-branch]: https://codecov.io/gh/boto/botocore/branch/develop?src=pr
[cc-compare]: https://codecov.io/gh/boto/botocore/compare/5ede9129082d87e7db417d5b157aa5489f0634d2...97f1a32b2141a2e53bd77318c6de04affa5d52d2
[cc-pull]: https://codecov.io/gh/boto/botocore/pull/964?src=pr
kyleknap: Looks good. Had some refactoring comments and comment about adding more tests. It would also be good to add a functional for unauthenticated posts and urls. I know we have integration tests for those, but we do not need those for the unauthenticated case.
kyleknap: I think it looks good. Just one more test I think we should add.
JordonPhillips: Updated. One thing I know you're going to ask about is 'where is fix_s3_host'. The answer is that since we're going through `sign`, `before-sign` will be called, and thus `fix_s3_host` will be called from the standard handler.
jamesls: :shipit:  Nice, I think this looks much better.

Patch:
diff --git a/.changes/next-release/bugfix-RequestSigner-36076.json b/.changes/next-release/bugfix-RequestSigner-36076.json
new file mode 100644
index 000000000..5e46be110
--- /dev/null
+++ b/.changes/next-release/bugfix-RequestSigner-36076.json
@@ -0,0 +1,5 @@
+{
+  "category": "RequestSigner",
+  "type": "bugfix",
+  "description": "`RequestSigner.generate_presigned_url` now requires the operation name to be passed in. This does not affect using `generate_presigned_url` through a client."
+}
diff --git a/.changes/next-release/feature-RequestSigner-34181.json b/.changes/next-release/feature-RequestSigner-34181.json
new file mode 100644
index 000000000..0e5aab568
--- /dev/null
+++ b/.changes/next-release/feature-RequestSigner-34181.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "description": "Allow `botocore.UNSIGNED` to be used with `generate_presigned_url` and `generate_presigned_post`.",
+  "category": "RequestSigner"
+}
diff --git a/botocore/handlers.py b/botocore/handlers.py
index af61531c0..d5b5e993f 100644
--- a/botocore/handlers.py
+++ b/botocore/handlers.py
@@ -386,7 +386,8 @@ def copy_snapshot_encrypted(params, request_signer, **kwargs):
     request_dict_copy['method'] = 'GET'
     request_dict_copy['headers'] = {}
     presigned_url = request_signer.generate_presigned_url(
-        request_dict_copy, region_name=source_region)
+        request_dict_copy, region_name=source_region,
+        operation_name='CopySnapshot')
     params['PresignedUrl'] = presigned_url
 
 
diff --git a/botocore/signers.py b/botocore/signers.py
index bcabda516..0116cd7d9 100644
--- a/botocore/signers.py
+++ b/botocore/signers.py
@@ -89,7 +89,8 @@ class RequestSigner(object):
         # Don't call this method directly.
         return self.sign(operation_name, request)
 
-    def sign(self, operation_name, request):
+    def sign(self, operation_name, request, region_name=None,
+             signing_type='standard', expires_in=None):
         """Sign a request before it goes out over the wire.
 
         :type operation_name: string
@@ -97,18 +98,26 @@ class RequestSigner(object):
                                ``ListBuckets``.
         :type request: AWSRequest
         :param request: The request object to be sent over the wire.
+
+        :type region_name: str
+        :param region_name: The region to sign the request for.
+
+        :type signing_type: str
+        :param signing_type: The type of signing to perform. This can be one of
+            three possible values:
+
+            * 'standard'     - This should be used for most requests.
+            * 'presign-url'  - This should be used when pre-signing a request.
+            * 'presign-post' - This should be used when pre-signing an S3 post.
+
+        :type expires_in: int
+        :param expires_in: The number of seconds the presigned url is valid
+            for. This parameter is only valid for signing type 'presign-url'.
         """
-        signature_version = self._signature_version
+        if region_name is None:
+            region_name = self._region_name
 
-        # Allow overriding signature version. A response of a blank
-        # string means no signing is performed. A response of ``None``
-        # means that the default signing method is used.
-        handler, response = self._event_emitter.emit_until_response(
-            'choose-signer.{0}.{1}'.format(self._service_name, operation_name),
-            signing_name=self._signing_name, region_name=self._region_name,
-            signature_version=signature_version)
-        if response is not None:
-            signature_version = response
+        signature_version = self._choose_signer(operation_name, signing_type)
 
         # Allow mutating request before signing
         self._event_emitter.emit(
@@ -118,10 +127,60 @@ class RequestSigner(object):
             signature_version=signature_version, request_signer=self)
 
         if signature_version != botocore.UNSIGNED:
-            signer = self.get_auth_instance(self._signing_name,
-                                            self._region_name,
-                                            signature_version)
-            signer.add_auth(request=request)
+            kwargs = {
+                'signing_name': self._signing_name,
+                'region_name': region_name,
+                'signature_version': signature_version
+            }
+            if expires_in is not None:
+                kwargs['expires'] = expires_in
+
+            try:
+                auth = self.get_auth_instance(**kwargs)
+            except UnknownSignatureVersionError as e:
+                if signing_type != 'standard':
+                    raise UnsupportedSignatureVersionError(
+                        signature_version=signature_version)
+                else:
+                    raise e
+
+            auth.add_auth(request)
+
+    def _choose_signer(self, operation_name, signing_type):
+        """
+        Allow setting the signature version via the choose-signer event.
+        A value of `botocore.UNSIGNED` means no signing will be performed.
+
+        :param operation_name: The operation to sign.
+        :param signing_type: The type of signing that the signer is to be used
+            for.
+        :return: The signature version to sign with.
+        """
+        signing_type_suffix_map = {
+            'presign-post': '-presign-post',
+            'presign-url': '-query'
+        }
+        suffix = signing_type_suffix_map.get(signing_type, '')
+
+        signature_version = self._signature_version
+        if signature_version is not botocore.UNSIGNED and not \
+                signature_version.endswith(suffix):
+            signature_version += suffix
+
+        handler, response = self._event_emitter.emit_until_response(
+            'choose-signer.{0}.{1}'.format(self._service_name, operation_name),
+            signing_name=self._signing_name, region_name=self._region_name,
+            signature_version=signature_version)
+
+        if response is not None:
+            signature_version = response
+            # The suffix needs to be checked again in case we get an improper
+            # signature version from choose-signer.
+            if signature_version is not botocore.UNSIGNED and not \
+                    signature_version.endswith(suffix):
+                signature_version += suffix
+
+        return signature_version
 
     def get_auth_instance(self, signing_name, region_name,
                           signature_version=None, **kwargs):
@@ -169,14 +228,17 @@ class RequestSigner(object):
     # Alias get_auth for backwards compatibility.
     get_auth = get_auth_instance
 
-    def generate_presigned_url(self, request_dict, expires_in=3600,
-                               region_name=None):
+    def generate_presigned_url(self, request_dict, operation_name,
+                               expires_in=3600, region_name=None):
         """Generates a presigned url
 
         :type request_dict: dict
         :param request_dict: The prepared request dictionary returned by
             ``botocore.awsrequest.prepare_request_dict()``
 
+        :type operation_name: str
+        :param operation_name: The operation being signed.
+
         :type expires_in: int
         :param expires_in: The number of seconds the presigned url is valid
             for. By default it expires in an hour (3600 seconds)
@@ -186,33 +248,11 @@ class RequestSigner(object):
 
         :returns: The presigned url
         """
-        if region_name is None:
-            region_name = self._region_name
-        query_prefix = '-query'
-        signature_version = self._signature_version
-        if not signature_version.endswith(query_prefix):
-            signature_version += query_prefix
-
-        kwargs = {'signing_name': self._signing_name,
-                  'region_name': region_name,
-                  'signature_version': signature_version,
-                  'expires': expires_in}
-
-        signature_type = signature_version.split('-', 1)[0]
-        try:
-            auth = self.get_auth_instance(**kwargs)
-        except UnknownSignatureVersionError:
-            raise UnsupportedSignatureVersionError(
-                signature_version=signature_type)
-
         request = create_request_object(request_dict)
+        self.sign(operation_name, request, region_name,
+                  'presign-url', expires_in)
 
-        # Fix s3 host for s3 sigv2 bucket names
-        fix_s3_host(request, signature_type, region_name)
-
-        auth.add_auth(request)
         request.prepare()
-
         return request.url
 
 
@@ -394,9 +434,6 @@ class S3PostPresigner(object):
         if conditions is None:
             conditions = []
 
-        if region_name is None:
-            region_name = self._request_signer.region_name
-
         # Create the policy for the post.
         policy = {}
 
@@ -410,33 +447,13 @@ class S3PostPresigner(object):
         for condition in conditions:
             policy['conditions'].append(condition)
 
-        # Obtain the appropriate signer.
-        query_prefix = '-presign-post'
-        signature_version = self._request_signer.signature_version
-        if not signature_version.endswith(query_prefix):
-            signature_version += query_prefix
-
-        kwargs = {'signing_name': self._request_signer.signing_name,
-                  'region_name': region_name,
-                  'signature_version': signature_version}
-
-        signature_type = signature_version.split('-', 1)[0]
-
-        try:
-            auth = self._request_signer.get_auth_instance(**kwargs)
-        except UnknownSignatureVersionError:
-            raise UnsupportedSignatureVersionError(
-                signature_version=signature_type)
-
         # Store the policy and the fields in the request for signing
         request = create_request_object(request_dict)
         request.context['s3-presign-post-fields'] = fields
         request.context['s3-presign-post-policy'] = policy
 
-        auth.add_auth(request)
-
-        # Fix s3 host for s3 sigv2 bucket names
-        fix_s3_host(request, signature_type, region_name)
+        self._request_signer.sign(
+            'PutObject', request, region_name, 'presign-post')
         # Return the url and the fields for th form to post.
         return {'url': request.url, 'fields': fields}
 
@@ -496,7 +513,8 @@ def generate_presigned_url(self, ClientMethod, Params=None, ExpiresIn=3600,
 
     # Generate the presigned url.
     return request_signer.generate_presigned_url(
-        request_dict=request_dict, expires_in=expires_in)
+        request_dict=request_dict, expires_in=expires_in,
+        operation_name=operation_name)
 
 
 def add_generate_presigned_post(class_attributes, **kwargs):
diff --git a/botocore/utils.py b/botocore/utils.py
index b18eccc35..3471a87ea 100644
--- a/botocore/utils.py
+++ b/botocore/utils.py
@@ -24,6 +24,7 @@ from six import string_types, text_type
 import dateutil.parser
 from dateutil.tz import tzlocal, tzutc
 
+import botocore
 from botocore.exceptions import InvalidExpressionError, ConfigNotFound
 from botocore.exceptions import InvalidDNSNameError, ClientError
 from botocore.compat import json, quote, zip_longest, urlsplit, urlunsplit
@@ -645,7 +646,8 @@ def fix_s3_host(request, signature_version, region_name, **kwargs):
     """
     # By default we do not use virtual hosted style addressing when
     # signed with signature version 4.
-    if signature_version in ['s3v4', 'v4']:
+    if signature_version is not botocore.UNSIGNED and \
+            's3v4' in signature_version:
         return
     elif not _allowed_region(region_name):
         return


Test patch:
diff --git a/tests/functional/test_s3.py b/tests/functional/test_s3.py
index a3619526a..890487f25 100644
--- a/tests/functional/test_s3.py
+++ b/tests/functional/test_s3.py
@@ -423,3 +423,26 @@ class TestRegionRedirect(BaseS3OperationTest):
                      '?encoding-type=url')
         self.assertEqual(calls[1].url, fixed_url)
         self.assertEqual(calls[2].url, fixed_url)
+
+
+class TestGeneratePresigned(BaseS3OperationTest):
+    def test_generate_unauthed_url(self):
+        config = Config(signature_version=botocore.UNSIGNED)
+        client = self.session.create_client('s3', self.region, config=config)
+        url = client.generate_presigned_url(
+            ClientMethod='get_object',
+            Params={
+                'Bucket': 'foo',
+                'Key': 'bar'
+            })
+        self.assertEqual(url, 'https://foo.s3.amazonaws.com/bar')
+
+    def test_generate_unauthed_post(self):
+        config = Config(signature_version=botocore.UNSIGNED)
+        client = self.session.create_client('s3', self.region, config=config)
+        parts = client.generate_presigned_post(Bucket='foo', Key='bar')
+        expected = {
+            'fields': {'key': 'bar'},
+            'url': 'https://foo.s3.amazonaws.com/'
+        }
+        self.assertEqual(parts, expected)
diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py
index 43f224ee2..dcec9f240 100644
--- a/tests/unit/test_handlers.py
+++ b/tests/unit/test_handlers.py
@@ -137,8 +137,9 @@ class TestHandlers(BaseSessionTest):
 
     def test_copy_snapshot_encrypted(self):
         credentials = Credentials('key', 'secret')
+        event_emitter = HierarchicalEmitter()
         request_signer = RequestSigner(
-            'ec2', 'us-east-1', 'ec2', 'v4', credentials, mock.Mock())
+            'ec2', 'us-east-1', 'ec2', 'v4', credentials, event_emitter)
         request_dict = {}
         params = {'SourceRegion': 'us-west-2'}
         request_dict['body'] = params
@@ -164,8 +165,9 @@ class TestHandlers(BaseSessionTest):
         actual_region = 'us-west-1'
 
         credentials = Credentials('key', 'secret')
+        event_emitter = HierarchicalEmitter()
         request_signer = RequestSigner(
-            'ec2', actual_region, 'ec2', 'v4', credentials, mock.Mock())
+            'ec2', actual_region, 'ec2', 'v4', credentials, event_emitter)
         request_dict = {}
         params = {
             'SourceRegion': 'us-west-2',
diff --git a/tests/unit/test_signers.py b/tests/unit/test_signers.py
index 591b337aa..296c283ae 100644
--- a/tests/unit/test_signers.py
+++ b/tests/unit/test_signers.py
@@ -25,6 +25,7 @@ from botocore.exceptions import NoRegionError, UnknownSignatureVersionError
 from botocore.exceptions import UnknownClientMethodError, ParamValidationError
 from botocore.exceptions import UnsupportedSignatureVersionError
 from botocore.signers import RequestSigner, S3PostPresigner, CloudFrontSigner
+from botocore.config import Config
 
 from tests import unittest
 
@@ -112,7 +113,7 @@ class TestSigner(BaseSignerTest):
             self.signer.sign('operation_name', request)
 
         auth.assert_called_with(credentials=self.fixed_credentials)
-        auth.return_value.add_auth.assert_called_with(request=request)
+        auth.return_value.add_auth.assert_called_with(request)
 
     def test_emits_before_sign(self):
         request = mock.Mock()
@@ -140,6 +141,62 @@ class TestSigner(BaseSignerTest):
 
         auth.assert_not_called()
 
+    def test_generate_url_emits_choose_signer(self):
+        request_dict = {
+            'headers': {},
+            'url': 'https://foo.com',
+            'body': b'',
+            'url_path': '/',
+            'method': 'GET',
+            'context': {}
+        }
+
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
+                             {'v4': mock.Mock()}):
+            self.signer.generate_presigned_url(request_dict, 'operation_name')
+
+        self.emitter.emit_until_response.assert_called_with(
+            'choose-signer.service_name.operation_name',
+            signing_name='signing_name', region_name='region_name',
+            signature_version='v4-query')
+
+    def test_generate_url_choose_signer_override(self):
+        request_dict = {
+            'headers': {},
+            'url': 'https://foo.com',
+            'body': b'',
+            'url_path': '/',
+            'method': 'GET',
+            'context': {}
+        }
+        auth = mock.Mock()
+        auth.REQUIRES_REGION = False
+        self.emitter.emit_until_response.return_value = (None, 'custom')
+
+        auth_types_map = {'custom': mock.Mock(), 'custom-query': auth}
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types_map):
+            self.signer.generate_presigned_url(request_dict, 'operation_name')
+
+        auth.assert_called_with(credentials=self.fixed_credentials,
+                                expires=3600)
+
+    def test_generate_url_unsigned(self):
+        request_dict = {
+            'headers': {},
+            'url': 'https://foo.com',
+            'body': b'',
+            'url_path': '/',
+            'method': 'GET',
+            'context': {}
+        }
+        self.emitter.emit_until_response.return_value = (
+            None, botocore.UNSIGNED)
+
+        url = self.signer.generate_presigned_url(
+            request_dict, 'operation_name')
+
+        self.assertEqual(url, 'https://foo.com')
+
     def test_generate_presigned_url(self):
         auth = mock.Mock()
         auth.REQUIRES_REGION = True
@@ -154,7 +211,8 @@ class TestSigner(BaseSignerTest):
         }
         with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
                              {'v4-query': auth}):
-            presigned_url = self.signer.generate_presigned_url(request_dict)
+            presigned_url = self.signer.generate_presigned_url(
+                request_dict, operation_name='operation_name')
         auth.assert_called_with(
             credentials=self.fixed_credentials, region_name='region_name',
             service_name='signing_name', expires=3600)
@@ -175,7 +233,8 @@ class TestSigner(BaseSignerTest):
         with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
                              {'v4-query': auth}):
             presigned_url = self.signer.generate_presigned_url(
-                request_dict, region_name='us-west-2')
+                request_dict, operation_name='operation_name',
+                region_name='us-west-2')
         auth.assert_called_with(
             credentials=self.fixed_credentials, region_name='us-west-2',
             service_name='signing_name', expires=3600)
@@ -196,21 +255,14 @@ class TestSigner(BaseSignerTest):
         with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
                              {'v4-query': auth}):
             presigned_url = self.signer.generate_presigned_url(
-                request_dict, expires_in=900)
+                request_dict, operation_name='operation_name', expires_in=900)
         auth.assert_called_with(
             credentials=self.fixed_credentials,
             region_name='region_name',
             expires=900, service_name='signing_name')
         self.assertEqual(presigned_url, 'https://foo.com')
 
-    def test_generate_presigned_url_fixes_s3_host(self):
-        self.signer = RequestSigner(
-            'service_name', 'region_name', 'signing_name',
-            's3', self.credentials, self.emitter)
-
-        auth = mock.Mock()
-        auth.REQUIRES_REGION = True
-
+    def test_presigned_url_throws_unsupported_signature_error(self):
         request_dict = {
             'headers': {},
             'url': 'https://s3.amazonaws.com/mybucket/myobject',
@@ -219,22 +271,12 @@ class TestSigner(BaseSignerTest):
             'method': 'GET',
             'context': {}
         }
-        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
-                             {'s3-query': auth}):
-            presigned_url = self.signer.generate_presigned_url(
-                request_dict, expires_in=900)
-        auth.assert_called_with(
-            credentials=self.fixed_credentials, region_name='region_name',
-            expires=900, service_name='signing_name')
-        self.assertEqual(presigned_url,
-                         'https://mybucket.s3.amazonaws.com/myobject')
-
-    def test_presigned_url_throws_unsupported_signature_error(self):
         self.signer = RequestSigner(
             'service_name', 'region_name', 'signing_name',
             'foo', self.credentials, self.emitter)
         with self.assertRaises(UnsupportedSignatureVersionError):
-            self.signer.generate_presigned_url({})
+            self.signer.generate_presigned_url(
+                request_dict, operation_name='foo')
 
     def test_signer_with_refreshable_credentials_gets_credential_set(self):
         class FakeCredentials(Credentials):
@@ -277,6 +319,126 @@ class TestSigner(BaseSignerTest):
                 credentials=None,
             )
 
+    def test_sign_with_signing_type_standard(self):
+        auth = mock.Mock()
+        post_auth = mock.Mock()
+        query_auth = mock.Mock()
+        request = mock.Mock()
+        auth_types = {
+            'v4-presign-post': post_auth,
+            'v4-query': query_auth,
+            'v4': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.sign('operation_name', request,
+                             signing_type='standard')
+        self.assertFalse(post_auth.called)
+        self.assertFalse(query_auth.called)
+        auth.assert_called_with(
+            credentials=ReadOnlyCredentials('key', 'secret', None),
+            service_name='signing_name',
+            region_name='region_name'
+        )
+
+    def test_sign_with_signing_type_presign_url(self):
+        auth = mock.Mock()
+        post_auth = mock.Mock()
+        query_auth = mock.Mock()
+        request = mock.Mock()
+        auth_types = {
+            'v4-presign-post': post_auth,
+            'v4-query': query_auth,
+            'v4': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.sign('operation_name', request,
+                             signing_type='presign-url')
+        self.assertFalse(post_auth.called)
+        self.assertFalse(auth.called)
+        query_auth.assert_called_with(
+            credentials=ReadOnlyCredentials('key', 'secret', None),
+            service_name='signing_name',
+            region_name='region_name'
+        )
+
+    def test_sign_with_signing_type_presign_post(self):
+        auth = mock.Mock()
+        post_auth = mock.Mock()
+        query_auth = mock.Mock()
+        request = mock.Mock()
+        auth_types = {
+            'v4-presign-post': post_auth,
+            'v4-query': query_auth,
+            'v4': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.sign('operation_name', request,
+                             signing_type='presign-post')
+        self.assertFalse(auth.called)
+        self.assertFalse(query_auth.called)
+        post_auth.assert_called_with(
+            credentials=ReadOnlyCredentials('key', 'secret', None),
+            service_name='signing_name',
+            region_name='region_name'
+        )
+
+    def test_sign_with_region_name(self):
+        request = mock.Mock()
+        auth = mock.Mock()
+        auth_types = {
+            'v4': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.sign('operation_name', request, region_name='foo')
+        auth.assert_called_with(
+            credentials=ReadOnlyCredentials('key', 'secret', None),
+            service_name='signing_name',
+            region_name='foo'
+        )
+
+    def test_sign_with_expires_in(self):
+        request = mock.Mock()
+        auth = mock.Mock()
+        auth_types = {
+            'v4': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.sign('operation_name', request, expires_in=2)
+        auth.assert_called_with(
+            credentials=ReadOnlyCredentials('key', 'secret', None),
+            service_name='signing_name',
+            region_name='region_name',
+            expires=2
+        )
+
+    def test_unknown_signer_raises_unknown_on_standard(self):
+        request = mock.Mock()
+        auth = mock.Mock()
+        auth_types = {
+            'v4': auth
+        }
+        self.emitter.emit_until_response.return_value = (None, 'custom')
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            with self.assertRaises(UnknownSignatureVersionError):
+                self.signer.sign('operation_name', request,
+                                 signing_type='standard')
+
+    def test_unknown_signer_raises_unsupported_when_not_standard(self):
+        request = mock.Mock()
+        auth = mock.Mock()
+        auth_types = {
+            'v4': auth
+        }
+        self.emitter.emit_until_response.return_value = (None, 'custom')
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            with self.assertRaises(UnsupportedSignatureVersionError):
+                self.signer.sign('operation_name', request,
+                                 signing_type='presign-url')
+
+            with self.assertRaises(UnsupportedSignatureVersionError):
+                self.signer.sign('operation_name', request,
+                                 signing_type='presign-post')
+
 
 class TestCloudfrontSigner(unittest.TestCase):
     def setUp(self):
@@ -401,6 +563,63 @@ class TestS3PostPresigner(BaseSignerTest):
                          'https://s3.amazonaws.com/mybucket')
         self.assertEqual(post_form_args['fields'], {})
 
+    def test_generate_presigned_post_emits_choose_signer(self):
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
+                             {'s3v4-presign-post': self.auth}):
+            self.signer.generate_presigned_post(self.request_dict)
+        self.emitter.emit_until_response.assert_called_with(
+            'choose-signer.service_name.PutObject',
+            signing_name='signing_name', region_name='region_name',
+            signature_version='s3v4-presign-post')
+
+    def test_generate_presigned_post_choose_signer_override(self):
+        auth = mock.Mock()
+        self.emitter.emit_until_response.return_value = (None, 'custom')
+        auth_types = {
+            's3v4-presign-post': self.auth,
+            'custom-presign-post': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.generate_presigned_post(self.request_dict)
+        auth.assert_called_with(
+            credentials=self.fixed_credentials, region_name='region_name',
+            service_name='signing_name')
+
+    def test_generate_presigne_post_choose_signer_override_known(self):
+        auth = mock.Mock()
+        self.emitter.emit_until_response.return_value = (
+            None, 's3v4-presign-post')
+        auth_types = {
+            's3v4-presign-post': self.auth,
+            'custom-presign-post': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            self.signer.generate_presigned_post(self.request_dict)
+        self.auth.assert_called_with(
+            credentials=self.fixed_credentials, region_name='region_name',
+            service_name='signing_name')
+
+    def test_generate_presigned_post_bad_signer_raises_error(self):
+        auth = mock.Mock()
+        self.emitter.emit_until_response.return_value = (None, 's3-query')
+        auth_types = {
+            's3v4-presign-post': self.auth,
+            's3-query': auth
+        }
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS, auth_types):
+            with self.assertRaises(UnsupportedSignatureVersionError):
+                self.signer.generate_presigned_post(self.request_dict)
+
+    def test_generate_unsigned_post(self):
+        self.emitter.emit_until_response.return_value = (
+            None, botocore.UNSIGNED)
+        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
+                             {'s3v4-presign-post': self.auth}):
+            post_form_args = self.signer.generate_presigned_post(
+                self.request_dict)
+        expected = {'fields': {}, 'url': 'https://s3.amazonaws.com/mybucket'}
+        self.assertEqual(post_form_args, expected)
+
     def test_generate_presigned_post_with_conditions(self):
         conditions = [
             {'bucket': 'mybucket'},
@@ -427,29 +646,21 @@ class TestS3PostPresigner(BaseSignerTest):
             credentials=self.fixed_credentials, region_name='foo',
             service_name='signing_name')
 
-    def test_generate_presigned_post_fixes_s3_host(self):
-        self.request_signer = RequestSigner(
-            'service_name', 'region_name', 'signing_name',
-            's3', self.credentials, self.emitter)
-        self.signer = S3PostPresigner(self.request_signer)
-
-        with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
-                             {'s3-presign-post': self.auth}):
-            post_form_args = self.signer.generate_presigned_post(
-                self.request_dict)
-        self.auth.assert_called_with(
-            credentials=self.fixed_credentials, region_name='region_name',
-            service_name='signing_name')
-        self.assertEqual(post_form_args['url'],
-                         'https://mybucket.s3.amazonaws.com/')
-
     def test_presigned_post_throws_unsupported_signature_error(self):
+        request_dict = {
+            'headers': {},
+            'url': 'https://s3.amazonaws.com/mybucket/myobject',
+            'body': b'',
+            'url_path': '/',
+            'method': 'POST',
+            'context': {}
+        }
         self.request_signer = RequestSigner(
             'service_name', 'region_name', 'signing_name',
             'foo', self.credentials, self.emitter)
         self.signer = S3PostPresigner(self.request_signer)
         with self.assertRaises(UnsupportedSignatureVersionError):
-            self.signer.generate_presigned_post({})
+            self.signer.generate_presigned_post(request_dict)
 
 
 class TestGenerateUrl(unittest.TestCase):
@@ -479,7 +690,8 @@ class TestGenerateUrl(unittest.TestCase):
             'method': u'GET',
             'context': {}}
         self.generate_url_mock.assert_called_with(
-            request_dict=ref_request_dict, expires_in=3600)
+            request_dict=ref_request_dict, expires_in=3600,
+            operation_name='GetObject')
 
     def test_generate_presigned_url_with_query_string(self):
         disposition = 'attachment; filename="download.jpg"'
@@ -499,7 +711,8 @@ class TestGenerateUrl(unittest.TestCase):
             'method': u'GET',
             'context': {}}
         self.generate_url_mock.assert_called_with(
-            request_dict=ref_request_dict, expires_in=3600)
+            request_dict=ref_request_dict, expires_in=3600,
+            operation_name='GetObject')
 
     def test_generate_presigned_url_unknown_method_name(self):
         with self.assertRaises(UnknownClientMethodError):
@@ -522,7 +735,8 @@ class TestGenerateUrl(unittest.TestCase):
             'method': u'GET',
             'context': {}}
         self.generate_url_mock.assert_called_with(
-            request_dict=ref_request_dict, expires_in=20)
+            request_dict=ref_request_dict, expires_in=20,
+            operation_name='GetObject')
 
     def test_generate_presigned_url_override_http_method(self):
         self.client.generate_presigned_url(
@@ -537,7 +751,8 @@ class TestGenerateUrl(unittest.TestCase):
             'method': u'PUT',
             'context': {}}
         self.generate_url_mock.assert_called_with(
-            request_dict=ref_request_dict, expires_in=3600)
+            request_dict=ref_request_dict, expires_in=3600,
+            operation_name='GetObject')
 
 
 class TestGeneratePresignedPost(unittest.TestCase):