Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement opt-in unsigned requests for clients. #448

Merged
merged 3 commits into from
Feb 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions botocore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def emit(self, record):

BOTOCORE_ROOT = os.path.dirname(os.path.abspath(__file__))

# Used to specify anonymous (unsigned) request signature
UNSIGNED = object()


def xform_name(name, sep='_', _xform_cache=_xform_cache):
"""Convert camel case to a "pythonic" name.
Expand Down
22 changes: 19 additions & 3 deletions botocore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ def __init__(self, loader, endpoint_resolver, user_agent, event_emitter,

def create_client(self, service_name, region_name, is_secure=True,
endpoint_url=None, verify=None,
credentials=None, scoped_config=None):
credentials=None, scoped_config=None,
client_config=None):
service_model = self._load_service_model(service_name)
cls = self.create_client_class(service_name)
client_args = self._get_client_args(
service_model, region_name, is_secure, endpoint_url,
verify, credentials, scoped_config)
verify, credentials, scoped_config, client_config)
return cls(**client_args)

def create_client_class(self, service_name):
Expand Down Expand Up @@ -209,7 +210,7 @@ def _get_signature_version_and_region(self, service_model, region_name,

def _get_client_args(self, service_model, region_name, is_secure,
endpoint_url, verify, credentials,
scoped_config):
scoped_config, client_config):
# A client needs:
#
# * serializer
Expand All @@ -232,6 +233,9 @@ def _get_client_args(self, service_model, region_name, is_secure,
self._get_signature_version_and_region(
service_model, region_name, is_secure, scoped_config)

if client_config and client_config.signature_version is not None:
signature_version = client_config.signature_version

signer = RequestSigner(service_model.service_name, region_name,
service_model.signing_name,
signature_version, credentials,
Expand Down Expand Up @@ -378,3 +382,15 @@ def __init__(self, events):
def __copy__(self):
copied_events = copy.copy(self.events)
return ClientMeta(copied_events)


class Config(object):
"""Advanced configuration for Botocore clients.

This class allows you to configure:

* Signature version

"""
def __init__(self, signature_version=None):
self.signature_version = signature_version
6 changes: 3 additions & 3 deletions botocore/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from botocore import retryhandler
from botocore import utils
from botocore import translate
import botocore
import botocore.auth


Expand Down Expand Up @@ -269,10 +270,9 @@ def _register_for_operations(config, session, service_name):
def disable_signing(**kwargs):
"""
This handler disables request signing by setting the signer
name to an empty string, similar to how the signature
overrides above work.
name to a special sentinel value.
"""
return ''
return botocore.UNSIGNED


def add_expect_header(model, params, **kwargs):
Expand Down
8 changes: 6 additions & 2 deletions botocore/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ def lazy_register_component(self, name, component):
def create_client(self, service_name, region_name=None, api_version=None,
use_ssl=True, verify=None, endpoint_url=None,
aws_access_key_id=None, aws_secret_access_key=None,
aws_session_token=None):
aws_session_token=None, config=None):
"""Create a botocore client.

:type service_name: string
Expand Down Expand Up @@ -805,6 +805,9 @@ def create_client(self, service_name, region_name=None, api_version=None,
:param aws_session_token: The session token to use when creating
the client. Same semantics as aws_access_key_id above.

:type config: botocore.client.Config
:param config: Advanced client configuration options.

:rtype: botocore.client.BaseClient
:return: A botocore client instance

Expand All @@ -828,7 +831,8 @@ def create_client(self, service_name, region_name=None, api_version=None,
response_parser_factory)
client = client_creator.create_client(
service_name, region_name, use_ssl, endpoint_url, verify,
credentials, scoped_config=self.get_scoped_config())
credentials, scoped_config=self.get_scoped_config(),
client_config=config)
return client


Expand Down
3 changes: 2 additions & 1 deletion botocore/signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import botocore
import botocore.auth

from botocore.exceptions import UnknownSignatureVersionError
Expand Down Expand Up @@ -86,7 +87,7 @@ def sign(self, operation_name, request):
signature_version=signature_version, request_signer=self)

# Sign the request if the signature version isn't None or blank
if signature_version:
if signature_version != botocore.UNSIGNED:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this affect the CLI? We set the endpoint's signature version to None. You can check real quick by running this test: https:/aws/aws-cli/blob/develop/tests/integration/customizations/s3/test_plugin.py#L1594

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given it's now a sentinel, we should be using identity checks instead of equality check (signature_version is not botocore.UNSIGNED)

signer = self.get_auth(self._signing_name, self._region_name,
signature_version)
signer.add_auth(request=request)
Expand Down
32 changes: 31 additions & 1 deletion tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from tests import unittest
import mock

import botocore
from botocore import client
from botocore import hooks
from botocore.credentials import Credentials
Expand Down Expand Up @@ -100,7 +101,7 @@ def test_client_signature_no_override(self, request_signer):
mock.ANY, mock.ANY, mock.ANY, 'v4', mock.ANY, mock.ANY)

@mock.patch('botocore.client.RequestSigner')
def test_client_signature_override(self, request_signer):
def test_client_signature_override_config_file(self, request_signer):
creator = self.create_client_creator()
config = {
'myservice': {'signature_version': 'foo'}
Expand All @@ -111,6 +112,35 @@ def test_client_signature_override(self, request_signer):
request_signer.assert_called_with(
mock.ANY, mock.ANY, mock.ANY, 'foo', mock.ANY, mock.ANY)

@mock.patch('botocore.client.RequestSigner')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be possible, but can we add a test that's more "direct"? I see that this test is essentially checking that we forward the value of config.signature_version to the request signer, but it'd be great if we had something that verified, if you set the signature version to UNSIGNED, then we do not sign the request. I get there's another level of indirection here with the RequestSigner and the auth classes, but it seems possible to regress on this feature without any unit tests failing because we're never actually testing anything that explicitly sets the value to botocore.UNSIGNED.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into adding a more comprehensive test for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there is another test that covers some of this:

https:/boto/botocore/blob/develop/tests/unit/test_signers.py#L117-L127

I've also added a new test to ensure the request is attempted without any auth headers/query args.

def test_client_signature_override_arg(self, request_signer):
creator = self.create_client_creator()
config = botocore.client.Config(signature_version='foo')
service_client = creator.create_client(
'myservice', 'us-west-2', credentials=self.credentials,
client_config=config)
request_signer.assert_called_with(
mock.ANY, mock.ANY, mock.ANY, 'foo', mock.ANY, mock.ANY)

def test_anonymous_client_request(self):
creator = self.create_client_creator()
config = botocore.client.Config(signature_version=botocore.UNSIGNED)
service_client = creator.create_client(
'myservice', 'us-west-2', client_config=config)

response = service_client.test_operation(Foo='one')

# Make sure a request has been attempted
self.assertTrue(self.endpoint.make_request.called)

# Make sure the request parameters do NOT include auth
# information. The service defined above for these tests
# uses sigv4 by default (which we disable).
params = dict((k.lower(), v) for k, v in \
self.endpoint.make_request.call_args[0][1].items())
self.assertNotIn('authorization', params)
self.assertNotIn('x-amz-signature', params)

def test_client_registers_request_created_handler(self):
event_emitter = mock.Mock()
creator = self.create_client_creator(event_emitter=event_emitter)
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import mock
import copy

import botocore
import botocore.session
from botocore.hooks import first_non_none_response
from botocore.awsrequest import AWSRequest
Expand Down Expand Up @@ -48,7 +49,7 @@ def test_cant_decode_quoted_jsondoc(self):
self.assertEqual(converted_value, value)

def test_disable_signing(self):
self.assertEqual(handlers.disable_signing(), '')
self.assertEqual(handlers.disable_signing(), botocore.UNSIGNED)

def test_quote_source_header(self):
for op in ('UploadPartCopy', 'CopyObject'):
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ def test_credential_provider_not_called_when_creds_provided(self):
"explicit credentials were provided to the "
"create_client call.")

@mock.patch('botocore.client.ClientCreator')
def test_config_passed_to_client_creator(self, client_creator):
config = client.Config()
self.session.create_client('sts', config=config)

client_creator.return_value.create_client.assert_called_with(
mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
scoped_config=mock.ANY, client_config=config)

class TestPerformOperation(BaseSessionTest):
def test_s3(self):
service = self.session.get_service('s3')
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/test_signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import mock

import botocore
import botocore.auth

from botocore.credentials import Credentials
Expand Down Expand Up @@ -115,10 +116,11 @@ def test_emits_before_sign(self):
request_signer=self.signer)

def test_disable_signing(self):
# Returning a blank string from choose-signer disabled signing!
# Returning botocore.UNSIGNED from choose-signer disables signing!
request = mock.Mock()
auth = mock.Mock()
self.emitter.emit_until_response.return_value = (None, '')
self.emitter.emit_until_response.return_value = (None,
botocore.UNSIGNED)

with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
{'v4': auth}):
Expand Down