From 1ab82828232af4ad73a38d6fbef77cda546cee7a Mon Sep 17 00:00:00 2001 From: SamRemis Date: Tue, 6 Aug 2024 10:42:40 -0400 Subject: [PATCH 01/10] Multiauth Adds support for the new multi-auth trait that will allow a service or operation to specify a list of compatible authentication types --- .../enhancement-signing-83434.json | 5 ++ botocore/args.py | 3 + botocore/auth.py | 23 +++++- botocore/client.py | 17 +++-- botocore/config.py | 7 ++ botocore/configprovider.py | 6 ++ botocore/handlers.py | 14 +++- botocore/model.py | 11 +++ botocore/regions.py | 2 +- tests/functional/test_auth_config.py | 70 +++++++++++++++++++ tests/unit/test_auth.py | 36 ++++++++++ tests/unit/test_client.py | 23 ++++++ tests/unit/test_handlers.py | 22 +++++- 13 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 .changes/next-release/enhancement-signing-83434.json create mode 100644 tests/functional/test_auth_config.py create mode 100644 tests/unit/test_auth.py diff --git a/.changes/next-release/enhancement-signing-83434.json b/.changes/next-release/enhancement-signing-83434.json new file mode 100644 index 0000000000..50ac5492f2 --- /dev/null +++ b/.changes/next-release/enhancement-signing-83434.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "signing", + "description": "Adds support for the new 'auth' trait on our models that will allow a priority list of auth types for a service or operation" +} diff --git a/botocore/args.py b/botocore/args.py index 758a3c3c92..741ca77886 100644 --- a/botocore/args.py +++ b/botocore/args.py @@ -268,6 +268,9 @@ def compute_client_args( client_config.disable_request_compression ), client_context_params=client_config.client_context_params, + sigv4a_signing_region_set=( + client_config.sigv4a_signing_region_set + ), ) self._compute_retry_config(config_kwargs) self._compute_connect_timeout(config_kwargs) diff --git a/botocore/auth.py b/botocore/auth.py index 6b296cfaaa..66f41280fb 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -35,7 +35,8 @@ urlsplit, urlunsplit, ) -from botocore.exceptions import NoAuthTokenError, NoCredentialsError +from botocore.exceptions import NoAuthTokenError, NoCredentialsError, UnknownSignatureVersionError + from botocore.utils import ( is_valid_ipv6_endpoint_url, normalize_url_path, @@ -1160,3 +1161,23 @@ def add_auth(self, request): 's3v4-query': S3SigV4QueryAuth, } ) + +AUTH_TYPE_TO_SIGNATURE_VERSION = { + 'aws.auth#sigv4': 'v4', + 'aws.auth#sigv4a': 'v4a', + 'smithy.api#httpBearerAuth': 'bearer', + 'smithy.api#noAuth': 'none', +} + +def resolve_auth_type(auth_trait): + for auth_type in auth_trait: + if auth_type == 'smithy.api#noAuth': + return 'none' + elif auth_type in AUTH_TYPE_TO_SIGNATURE_VERSION: + signature_version = AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] + if signature_version in AUTH_TYPE_MAPS: + return signature_version + else: + raise UnknownSignatureVersionError( + signature_version=auth_type + ) diff --git a/botocore/client.py b/botocore/client.py index e57d1ded31..4b54a7df62 100644 --- a/botocore/client.py +++ b/botocore/client.py @@ -14,7 +14,7 @@ from botocore import waiter, xform_name from botocore.args import ClientArgsCreator -from botocore.auth import AUTH_TYPE_MAPS +from botocore.auth import AUTH_TYPE_MAPS, resolve_auth_type from botocore.awsrequest import prepare_request_dict from botocore.compress import maybe_compress_request from botocore.config import Config @@ -148,15 +148,18 @@ def create_client( region_name, client_config = self._normalize_fips_region( region_name, client_config ) + auth = service_model.metadata.get('auth') + if auth: + service_signature_version = resolve_auth_type(auth) + else: + service_signature_version = service_model.metadata.get('signatureVersion') endpoint_bridge = ClientEndpointBridge( self._endpoint_resolver, scoped_config, client_config, service_signing_name=service_model.metadata.get('signingName'), config_store=self._config_store, - service_signature_version=service_model.metadata.get( - 'signatureVersion' - ), + service_signature_version=service_signature_version, ) client_args = self._get_client_args( service_model, @@ -487,7 +490,7 @@ def _default_s3_presign_to_sigv2(self, signature_version, **kwargs): return if signature_version.startswith('v4-s3express'): - return f'{signature_version}' + return signature_version for suffix in ['-query', '-presign-post']: if signature_version.endswith(suffix): @@ -955,6 +958,10 @@ def _make_api_call(self, operation_name, api_params): 'has_streaming_input': operation_model.has_streaming_input, 'auth_type': operation_model.auth_type, } + + if operation_model.unsigned_payload: + request_context['payload_signing_enabled'] = False + api_params = self._emit_api_params( api_params=api_params, operation_model=operation_model, diff --git a/botocore/config.py b/botocore/config.py index 87b52b6f1a..86da65170d 100644 --- a/botocore/config.py +++ b/botocore/config.py @@ -221,6 +221,12 @@ class Config: Defaults to None. + :type sigv4a_signing_region_set: string + :param sigv4a_signing_region_set: Sets override for the signing region set + used for sigv4a signing + + Defaults to None. + :type client_context_params: dict :param client_context_params: A dictionary of parameters specific to individual services. If available, valid parameters can be found in @@ -256,6 +262,7 @@ class Config: ('tcp_keepalive', None), ('request_min_compression_size_bytes', None), ('disable_request_compression', None), + ('sigv4a_signing_region_set', None), ('client_context_params', None), ] ) diff --git a/botocore/configprovider.py b/botocore/configprovider.py index 5ed2dc63ce..b0dd09f09f 100644 --- a/botocore/configprovider.py +++ b/botocore/configprovider.py @@ -168,6 +168,12 @@ False, utils.ensure_boolean, ), + 'sigv4a_signing_region_set': ( + 'sigv4a_signing_region_set', + 'AWS_SIGV4A_SIGNING_REGION_SET', + None, + None, + ), } # A mapping for the s3 specific configuration vars. These are the configuration # vars that typically go in the s3 section of the config file. This mapping diff --git a/botocore/handlers.py b/botocore/handlers.py index 211ed0477c..54720b61c7 100644 --- a/botocore/handlers.py +++ b/botocore/handlers.py @@ -210,7 +210,11 @@ def set_operation_specific_signer(context, signing_name, **kwargs): if auth_type == 'v4a': # If sigv4a is chosen, we must add additional signing config for # global signature. - signing = {'region': '*', 'signing_name': signing_name} + region = _resolve_sigv4a_region(context) + signing = { + 'region': region, + 'signing_name': signing_name + } if 'signing' in context: context['signing'].update(signing) else: @@ -232,6 +236,14 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return signature_version +def _resolve_sigv4a_region(context): + region = None + if 'client_config' in context: + region = context['client_config'].sigv4a_signing_region_set + if not region and context.get('signing', {}).get('region'): + region = context['signing']['region'] + return region or '*' + def decode_console_output(parsed, **kwargs): if 'Output' in parsed: try: diff --git a/botocore/model.py b/botocore/model.py index df9159e36e..eda3aeda45 100644 --- a/botocore/model.py +++ b/botocore/model.py @@ -21,6 +21,7 @@ UndefinedModelAttributeError, ) from botocore.utils import CachedProperty, hyphenize_service_id, instance_cache +from botocore.auth import resolve_auth_type NOT_SET = object() @@ -623,10 +624,20 @@ def context_parameters(self): def request_compression(self): return self._operation_model.get('requestcompression') + @CachedProperty + def auth(self): + return self._operation_model.get('auth') + @CachedProperty def auth_type(self): + if self.auth: + return resolve_auth_type(self.auth) return self._operation_model.get('authtype') + @CachedProperty + def unsigned_payload(self): + return self._operation_model.get('unsignedPayload') + @CachedProperty def error_shapes(self): shapes = self._operation_model.get("errors", []) diff --git a/botocore/regions.py b/botocore/regions.py index ab20130304..755551c088 100644 --- a/botocore/regions.py +++ b/botocore/regions.py @@ -722,7 +722,7 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): signing_context['region'] = scheme['signingRegion'] elif 'signingRegionSet' in scheme: if len(scheme['signingRegionSet']) > 0: - signing_context['region'] = scheme['signingRegionSet'][0] + signing_context['region'] = ','.join(scheme['signingRegionSet']) if 'signingName' in scheme: signing_context.update(signing_name=scheme['signingName']) if 'disableDoubleEncoding' in scheme: diff --git a/tests/functional/test_auth_config.py b/tests/functional/test_auth_config.py new file mode 100644 index 0000000000..8f0a35fbad --- /dev/null +++ b/tests/functional/test_auth_config.py @@ -0,0 +1,70 @@ +# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. +import pytest + +from botocore.session import get_session + + +# In the future, some services may have a list of credentials requirements where one signature may fail and others may +# succeed, for instance a service may want to use bearer by default but fall back to sigv4 if a token isn't found. +# There currently is not a way to do this in botocore, so we added this test to make sure that we handle this gracefully +# when the need arises. +AUTH_TYPE_REQUIREMENTS={ + 'aws.auth#sigv4': ['credentials'], + 'aws.auth#sigv4a': ['credentials'], + 'smithy.api#httpBearerAuth': ['bearer_token'], + 'smithy.api#noAuth': [], +} + +def _all_test_cases(): + session = get_session() + loader = session.get_component('data_loader') + + services = loader.list_available_services('service-2') + auth_services = [] + auth_operations = [] + + for service in services: + service_model = session.get_service_model(service) + auth_config = service_model.metadata.get('auth', {}) + if auth_config: + auth_services.append([service, auth_config]) + for operation in service_model.operation_names: + operation_model = service_model.operation_model(operation) + if operation_model.auth: + auth_operations.append([service, operation_model]) + return auth_services, auth_operations + + +AUTH_SERVICES, AUTH_OPERATIONS = _all_test_cases() + +@pytest.mark.validates_models +@pytest.mark.parametrize("auth_service, auth_config", AUTH_SERVICES) +def test_all_requirements_match_for_service(auth_service, auth_config): + # Validates that all service-level signature types have the same requirements + message = f'Found mixed signer requirements for service: {auth_service}' + assert_all_requirements_match(auth_config, message) + +@pytest.mark.validates_models +@pytest.mark.parametrize("auth_service, operation_model", AUTH_OPERATIONS) +def test_all_requirements_match_for_operation(auth_service, operation_model): + # Validates that all operation-level signature types have the same requirements + message = f'Found mixed signer requirements for operation: {auth_service}.{operation_model.name}' + auth_config = operation_model.auth + assert_all_requirements_match(auth_config, message) + +def assert_all_requirements_match(auth_config, message): + if len(auth_config) > 1: + first_auth = auth_config.pop() + first_auth_reqs = AUTH_TYPE_REQUIREMENTS[first_auth] + assert all(first_auth_reqs == AUTH_TYPE_REQUIREMENTS[req] for req in auth_config), message \ No newline at end of file diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py new file mode 100644 index 0000000000..c452880c30 --- /dev/null +++ b/tests/unit/test_auth.py @@ -0,0 +1,36 @@ +# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. +import pytest + +from botocore.auth import AUTH_TYPE_MAPS, AUTH_TYPE_TO_SIGNATURE_VERSION, BaseSigner, resolve_auth_type +from tests import mock + + +#TODO probably put this test in a class +def test_auth_resolves_first_available(): + auth = ['aws.auth#foo', 'aws.auth#bar'] + bar_signer = mock.Mock(spec=BaseSigner) + + auth_types = AUTH_TYPE_MAPS.copy() + auth_types['bar'] = bar_signer + + auth_type_conversions = AUTH_TYPE_TO_SIGNATURE_VERSION.copy() + auth_type_conversions['aws.auth#foo'] = "foo" + auth_type_conversions['aws.auth#bar'] = "bar" + + with mock.patch('botocore.auth.AUTH_TYPE_MAPS', auth_types): + with mock.patch('botocore.auth.AUTH_TYPE_TO_SIGNATURE_VERSION', auth_type_conversions): + assert resolve_auth_type(auth) == 'bar' + + +#TODO add a test for the error case \ No newline at end of file diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 86974fd553..f71d3b854b 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -60,6 +60,16 @@ def setUp(self): 'input': {'shape': 'TestOperationRequest'}, 'errors': [{'shape': 'TestOperationException'}], 'documentation': 'Documents TestOperation', + }, + 'TestUnsignedOperation': { + 'name': 'TestUnsignedOperation', + 'http': { + 'method': 'POST', + 'requestUri': '/', + }, + 'input': {'shape': 'TestOperationRequest'}, + 'errors': [{'shape': 'TestOperationException'}], + 'documentation': 'Documents TestOperation', } }, 'shapes': { @@ -1713,6 +1723,19 @@ def test_client_internal_credential_shim(self): ) self.assertEqual(service_client._get_credentials(), self.credentials) + def test_unsigned_payload_disables_signing(self): + creator = self.create_client_creator() + service_client = creator.create_client( + 'myservice', 'us-west-2' + ) + test_operation = self.service_description['operations'][ + 'TestOperation' + ] + test_operation['unsignedPayload'] = True + service_client.test_operation(Foo='one') + context = self.endpoint.make_request.call_args[0][1]['context'] + self.assertFalse(context['payload_signing_enabled']) + class TestClientErrors(TestAutoGeneratedClient): def add_error_response(self, error_response): diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py index 44ebbecc3f..d9825eb9c5 100644 --- a/tests/unit/test_handlers.py +++ b/tests/unit/test_handlers.py @@ -1042,7 +1042,7 @@ def test_set_operation_specific_signer_v4a_existing_signing_context(self): context=context, signing_name=signing_name ) # region has been updated - self.assertEqual(context['signing']['region'], '*') + self.assertEqual(context['signing']['region'], 'abc') # signing_name has been added self.assertEqual(context['signing']['signing_name'], signing_name) # foo remained untouched @@ -1073,6 +1073,26 @@ def test_set_operation_specific_signer_s3v4_unsigned_payload(self): self.assertEqual(response, 's3v4') self.assertEqual(context.get('payload_signing_enabled'), False) + def test_set_operation_specific_signer_defaults_to_asterisk(self): + signing_name = 'myservice' + context = { + 'auth_type': 'v4a', + } + handlers.set_operation_specific_signer( + context=context, signing_name=signing_name + ) + self.assertEqual(context['signing']['region'], '*') + + def test_set_operation_specific_signer_prefers_client_config(self): + signing_name = 'myservice' + context = { + 'auth_type': 'v4a', + 'client_config': Config(sigv4a_signing_region_set="region_1,region_2") + } + handlers.set_operation_specific_signer( + context=context, signing_name=signing_name + ) + self.assertEqual(context['signing']['region'], 'region_1,region_2') @pytest.mark.parametrize( 'auth_type, expected_response', [('v4', 's3v4'), ('v4a', 's3v4a')] From fc0cb035c6c6f3a07b0a35b5ef67394ad140772c Mon Sep 17 00:00:00 2001 From: SamRemis Date: Tue, 6 Aug 2024 11:09:08 -0400 Subject: [PATCH 02/10] Test updates Run pre-commit and add test updates --- botocore/auth.py | 12 ++++--- botocore/client.py | 4 ++- botocore/handlers.py | 6 ++-- botocore/model.py | 2 +- botocore/regions.py | 4 ++- tests/functional/test_auth_config.py | 12 +++++-- tests/unit/auth/test_auth_trait.py | 48 ++++++++++++++++++++++++++++ tests/unit/test_auth.py | 36 --------------------- tests/unit/test_client.py | 6 ++-- tests/unit/test_handlers.py | 5 ++- 10 files changed, 79 insertions(+), 56 deletions(-) create mode 100644 tests/unit/auth/test_auth_trait.py delete mode 100644 tests/unit/test_auth.py diff --git a/botocore/auth.py b/botocore/auth.py index 66f41280fb..bb6f0705a4 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -35,8 +35,11 @@ urlsplit, urlunsplit, ) -from botocore.exceptions import NoAuthTokenError, NoCredentialsError, UnknownSignatureVersionError - +from botocore.exceptions import ( + NoAuthTokenError, + NoCredentialsError, + UnknownSignatureVersionError, +) from botocore.utils import ( is_valid_ipv6_endpoint_url, normalize_url_path, @@ -1169,6 +1172,7 @@ def add_auth(self, request): 'smithy.api#noAuth': 'none', } + def resolve_auth_type(auth_trait): for auth_type in auth_trait: if auth_type == 'smithy.api#noAuth': @@ -1178,6 +1182,4 @@ def resolve_auth_type(auth_trait): if signature_version in AUTH_TYPE_MAPS: return signature_version else: - raise UnknownSignatureVersionError( - signature_version=auth_type - ) + raise UnknownSignatureVersionError(signature_version=auth_type) diff --git a/botocore/client.py b/botocore/client.py index 4b54a7df62..999b00e8e1 100644 --- a/botocore/client.py +++ b/botocore/client.py @@ -152,7 +152,9 @@ def create_client( if auth: service_signature_version = resolve_auth_type(auth) else: - service_signature_version = service_model.metadata.get('signatureVersion') + service_signature_version = service_model.metadata.get( + 'signatureVersion' + ) endpoint_bridge = ClientEndpointBridge( self._endpoint_resolver, scoped_config, diff --git a/botocore/handlers.py b/botocore/handlers.py index 54720b61c7..b371aa5b78 100644 --- a/botocore/handlers.py +++ b/botocore/handlers.py @@ -211,10 +211,7 @@ def set_operation_specific_signer(context, signing_name, **kwargs): # If sigv4a is chosen, we must add additional signing config for # global signature. region = _resolve_sigv4a_region(context) - signing = { - 'region': region, - 'signing_name': signing_name - } + signing = {'region': region, 'signing_name': signing_name} if 'signing' in context: context['signing'].update(signing) else: @@ -244,6 +241,7 @@ def _resolve_sigv4a_region(context): region = context['signing']['region'] return region or '*' + def decode_console_output(parsed, **kwargs): if 'Output' in parsed: try: diff --git a/botocore/model.py b/botocore/model.py index eda3aeda45..873b547962 100644 --- a/botocore/model.py +++ b/botocore/model.py @@ -15,13 +15,13 @@ from collections import defaultdict from typing import NamedTuple, Union +from botocore.auth import resolve_auth_type from botocore.compat import OrderedDict from botocore.exceptions import ( MissingServiceIdError, UndefinedModelAttributeError, ) from botocore.utils import CachedProperty, hyphenize_service_id, instance_cache -from botocore.auth import resolve_auth_type NOT_SET = object() diff --git a/botocore/regions.py b/botocore/regions.py index 755551c088..cfa3bde115 100644 --- a/botocore/regions.py +++ b/botocore/regions.py @@ -722,7 +722,9 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): signing_context['region'] = scheme['signingRegion'] elif 'signingRegionSet' in scheme: if len(scheme['signingRegionSet']) > 0: - signing_context['region'] = ','.join(scheme['signingRegionSet']) + signing_context['region'] = ','.join( + scheme['signingRegionSet'] + ) if 'signingName' in scheme: signing_context.update(signing_name=scheme['signingName']) if 'disableDoubleEncoding' in scheme: diff --git a/tests/functional/test_auth_config.py b/tests/functional/test_auth_config.py index 8f0a35fbad..da14720e54 100644 --- a/tests/functional/test_auth_config.py +++ b/tests/functional/test_auth_config.py @@ -14,18 +14,18 @@ from botocore.session import get_session - # In the future, some services may have a list of credentials requirements where one signature may fail and others may # succeed, for instance a service may want to use bearer by default but fall back to sigv4 if a token isn't found. # There currently is not a way to do this in botocore, so we added this test to make sure that we handle this gracefully # when the need arises. -AUTH_TYPE_REQUIREMENTS={ +AUTH_TYPE_REQUIREMENTS = { 'aws.auth#sigv4': ['credentials'], 'aws.auth#sigv4a': ['credentials'], 'smithy.api#httpBearerAuth': ['bearer_token'], 'smithy.api#noAuth': [], } + def _all_test_cases(): session = get_session() loader = session.get_component('data_loader') @@ -48,6 +48,7 @@ def _all_test_cases(): AUTH_SERVICES, AUTH_OPERATIONS = _all_test_cases() + @pytest.mark.validates_models @pytest.mark.parametrize("auth_service, auth_config", AUTH_SERVICES) def test_all_requirements_match_for_service(auth_service, auth_config): @@ -55,6 +56,7 @@ def test_all_requirements_match_for_service(auth_service, auth_config): message = f'Found mixed signer requirements for service: {auth_service}' assert_all_requirements_match(auth_config, message) + @pytest.mark.validates_models @pytest.mark.parametrize("auth_service, operation_model", AUTH_OPERATIONS) def test_all_requirements_match_for_operation(auth_service, operation_model): @@ -63,8 +65,12 @@ def test_all_requirements_match_for_operation(auth_service, operation_model): auth_config = operation_model.auth assert_all_requirements_match(auth_config, message) + def assert_all_requirements_match(auth_config, message): if len(auth_config) > 1: first_auth = auth_config.pop() first_auth_reqs = AUTH_TYPE_REQUIREMENTS[first_auth] - assert all(first_auth_reqs == AUTH_TYPE_REQUIREMENTS[req] for req in auth_config), message \ No newline at end of file + assert all( + first_auth_reqs == AUTH_TYPE_REQUIREMENTS[req] + for req in auth_config + ), message diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py new file mode 100644 index 0000000000..9ed824c5eb --- /dev/null +++ b/tests/unit/auth/test_auth_trait.py @@ -0,0 +1,48 @@ +# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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 botocore.auth import ( + AUTH_TYPE_MAPS, + AUTH_TYPE_TO_SIGNATURE_VERSION, + BaseSigner, + resolve_auth_type, +) +from botocore.exceptions import ( + UnknownSignatureVersionError, +) +from tests import mock, unittest + +class TestAuthTraitResolution(unittest.TestCase): + + def test_auth_resolves_first_available(self): + auth = ['aws.auth#foo', 'aws.auth#bar'] + bar_signer = mock.Mock(spec=BaseSigner) + + auth_types = AUTH_TYPE_MAPS.copy() + auth_types['bar'] = bar_signer + + auth_type_conversions = AUTH_TYPE_TO_SIGNATURE_VERSION.copy() + auth_type_conversions['aws.auth#foo'] = "foo" + auth_type_conversions['aws.auth#bar'] = "bar" + + with mock.patch('botocore.auth.AUTH_TYPE_MAPS', auth_types): + with mock.patch( + 'botocore.auth.AUTH_TYPE_TO_SIGNATURE_VERSION', + auth_type_conversions, + ): + assert resolve_auth_type(auth) == 'bar' + + def test_invalid_auth_type_error(self): + auth = ['aws.auth#invalidAuth'] + with self.assertRaises(UnknownSignatureVersionError): + resolve_auth_type(auth) \ No newline at end of file diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py deleted file mode 100644 index c452880c30..0000000000 --- a/tests/unit/test_auth.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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. -import pytest - -from botocore.auth import AUTH_TYPE_MAPS, AUTH_TYPE_TO_SIGNATURE_VERSION, BaseSigner, resolve_auth_type -from tests import mock - - -#TODO probably put this test in a class -def test_auth_resolves_first_available(): - auth = ['aws.auth#foo', 'aws.auth#bar'] - bar_signer = mock.Mock(spec=BaseSigner) - - auth_types = AUTH_TYPE_MAPS.copy() - auth_types['bar'] = bar_signer - - auth_type_conversions = AUTH_TYPE_TO_SIGNATURE_VERSION.copy() - auth_type_conversions['aws.auth#foo'] = "foo" - auth_type_conversions['aws.auth#bar'] = "bar" - - with mock.patch('botocore.auth.AUTH_TYPE_MAPS', auth_types): - with mock.patch('botocore.auth.AUTH_TYPE_TO_SIGNATURE_VERSION', auth_type_conversions): - assert resolve_auth_type(auth) == 'bar' - - -#TODO add a test for the error case \ No newline at end of file diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index f71d3b854b..abb55853f3 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -70,7 +70,7 @@ def setUp(self): 'input': {'shape': 'TestOperationRequest'}, 'errors': [{'shape': 'TestOperationException'}], 'documentation': 'Documents TestOperation', - } + }, }, 'shapes': { 'TestOperationRequest': { @@ -1725,9 +1725,7 @@ def test_client_internal_credential_shim(self): def test_unsigned_payload_disables_signing(self): creator = self.create_client_creator() - service_client = creator.create_client( - 'myservice', 'us-west-2' - ) + service_client = creator.create_client('myservice', 'us-west-2') test_operation = self.service_description['operations'][ 'TestOperation' ] diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py index d9825eb9c5..1e3cac1411 100644 --- a/tests/unit/test_handlers.py +++ b/tests/unit/test_handlers.py @@ -1087,13 +1087,16 @@ def test_set_operation_specific_signer_prefers_client_config(self): signing_name = 'myservice' context = { 'auth_type': 'v4a', - 'client_config': Config(sigv4a_signing_region_set="region_1,region_2") + 'client_config': Config( + sigv4a_signing_region_set="region_1,region_2" + ), } handlers.set_operation_specific_signer( context=context, signing_name=signing_name ) self.assertEqual(context['signing']['region'], 'region_1,region_2') + @pytest.mark.parametrize( 'auth_type, expected_response', [('v4', 's3v4'), ('v4a', 's3v4a')] ) From 88495f15614a3d61461af3702b8185101c1e3363 Mon Sep 17 00:00:00 2001 From: SamRemis Date: Tue, 6 Aug 2024 11:09:50 -0400 Subject: [PATCH 03/10] run pre-commit --- tests/unit/auth/test_auth_trait.py | 8 +++----- tests/unit/test_client.py | 12 +----------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py index 9ed824c5eb..e25de02cf0 100644 --- a/tests/unit/auth/test_auth_trait.py +++ b/tests/unit/auth/test_auth_trait.py @@ -17,13 +17,11 @@ BaseSigner, resolve_auth_type, ) -from botocore.exceptions import ( - UnknownSignatureVersionError, -) +from botocore.exceptions import UnknownSignatureVersionError from tests import mock, unittest -class TestAuthTraitResolution(unittest.TestCase): +class TestAuthTraitResolution(unittest.TestCase): def test_auth_resolves_first_available(self): auth = ['aws.auth#foo', 'aws.auth#bar'] bar_signer = mock.Mock(spec=BaseSigner) @@ -45,4 +43,4 @@ def test_auth_resolves_first_available(self): def test_invalid_auth_type_error(self): auth = ['aws.auth#invalidAuth'] with self.assertRaises(UnknownSignatureVersionError): - resolve_auth_type(auth) \ No newline at end of file + resolve_auth_type(auth) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index abb55853f3..ad7143a521 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -60,17 +60,7 @@ def setUp(self): 'input': {'shape': 'TestOperationRequest'}, 'errors': [{'shape': 'TestOperationException'}], 'documentation': 'Documents TestOperation', - }, - 'TestUnsignedOperation': { - 'name': 'TestUnsignedOperation', - 'http': { - 'method': 'POST', - 'requestUri': '/', - }, - 'input': {'shape': 'TestOperationRequest'}, - 'errors': [{'shape': 'TestOperationException'}], - 'documentation': 'Documents TestOperation', - }, + } }, 'shapes': { 'TestOperationRequest': { From 7447d39d76107545c5c21c1d4c75fd53b3d7d697 Mon Sep 17 00:00:00 2001 From: SamRemis Date: Thu, 8 Aug 2024 13:20:53 -0400 Subject: [PATCH 04/10] Apply suggestions from code review Co-authored-by: Nate Prewitt --- .changes/next-release/enhancement-signing-83434.json | 2 +- botocore/auth.py | 2 +- botocore/config.py | 6 +++--- tests/functional/test_auth_config.py | 8 ++++---- tests/unit/auth/test_auth_trait.py | 11 +++-------- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.changes/next-release/enhancement-signing-83434.json b/.changes/next-release/enhancement-signing-83434.json index 50ac5492f2..6ec6206594 100644 --- a/.changes/next-release/enhancement-signing-83434.json +++ b/.changes/next-release/enhancement-signing-83434.json @@ -1,5 +1,5 @@ { "type": "enhancement", "category": "signing", - "description": "Adds support for the new 'auth' trait on our models that will allow a priority list of auth types for a service or operation" + "description": "Adds support for the new 'auth' trait on our models that will allow a priority list of auth types for a service or operation." } diff --git a/botocore/auth.py b/botocore/auth.py index bb6f0705a4..3961ed7e35 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -1176,7 +1176,7 @@ def add_auth(self, request): def resolve_auth_type(auth_trait): for auth_type in auth_trait: if auth_type == 'smithy.api#noAuth': - return 'none' + return AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] elif auth_type in AUTH_TYPE_TO_SIGNATURE_VERSION: signature_version = AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] if signature_version in AUTH_TYPE_MAPS: diff --git a/botocore/config.py b/botocore/config.py index 86da65170d..587dc95ad8 100644 --- a/botocore/config.py +++ b/botocore/config.py @@ -222,8 +222,8 @@ class Config: Defaults to None. :type sigv4a_signing_region_set: string - :param sigv4a_signing_region_set: Sets override for the signing region set - used for sigv4a signing + :param sigv4a_signing_region_set: A set of AWS regions to apply the signature for + when using SigV4a for signing. Set to ``*`` to represent all regions. Defaults to None. @@ -262,8 +262,8 @@ class Config: ('tcp_keepalive', None), ('request_min_compression_size_bytes', None), ('disable_request_compression', None), - ('sigv4a_signing_region_set', None), ('client_context_params', None), + ('sigv4a_signing_region_set', None), ] ) diff --git a/tests/functional/test_auth_config.py b/tests/functional/test_auth_config.py index da14720e54..f36798e153 100644 --- a/tests/functional/test_auth_config.py +++ b/tests/functional/test_auth_config.py @@ -14,10 +14,10 @@ from botocore.session import get_session -# In the future, some services may have a list of credentials requirements where one signature may fail and others may -# succeed, for instance a service may want to use bearer by default but fall back to sigv4 if a token isn't found. -# There currently is not a way to do this in botocore, so we added this test to make sure that we handle this gracefully -# when the need arises. +# In the future, a service may have a list of credentials requirements where one +# signature may fail and others may succeed. e.g. a service may want to use bearer +# auth but fall back to sigv4 if a token isn't available. There's currently no way to do +# this in botocore, so this test ensures we handle this gracefully when the need arises. AUTH_TYPE_REQUIREMENTS = { 'aws.auth#sigv4': ['credentials'], 'aws.auth#sigv4a': ['credentials'], diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py index e25de02cf0..97aa43b046 100644 --- a/tests/unit/auth/test_auth_trait.py +++ b/tests/unit/auth/test_auth_trait.py @@ -24,14 +24,9 @@ class TestAuthTraitResolution(unittest.TestCase): def test_auth_resolves_first_available(self): auth = ['aws.auth#foo', 'aws.auth#bar'] - bar_signer = mock.Mock(spec=BaseSigner) - - auth_types = AUTH_TYPE_MAPS.copy() - auth_types['bar'] = bar_signer - - auth_type_conversions = AUTH_TYPE_TO_SIGNATURE_VERSION.copy() - auth_type_conversions['aws.auth#foo'] = "foo" - auth_type_conversions['aws.auth#bar'] = "bar" + # Don't declare a signer for "foo" + auth_types = {'bar': mock.Mock(spec=BaseSigner)} + auth_type_conversions = {'aws.auth#foo': 'foo', 'aws.auth#bar': 'bar'} with mock.patch('botocore.auth.AUTH_TYPE_MAPS', auth_types): with mock.patch( From a7395c87c27fa4294058c783fe129eb631139f8b Mon Sep 17 00:00:00 2001 From: SamRemis Date: Thu, 8 Aug 2024 17:50:14 -0400 Subject: [PATCH 05/10] PR feedback implemented --- botocore/auth.py | 24 +++++++++---------- botocore/client.py | 6 ++--- botocore/handlers.py | 10 ++++---- botocore/model.py | 6 ++++- tests/functional/test_auth_config.py | 23 +++++++++--------- tests/unit/auth/test_auth_trait.py | 7 +----- tests/unit/test_client.py | 11 --------- tests/unit/test_handlers.py | 36 +++++++++++++++++++++++++--- tests/unit/test_model.py | 17 +++++++++++++ 9 files changed, 87 insertions(+), 53 deletions(-) diff --git a/botocore/auth.py b/botocore/auth.py index 3961ed7e35..70dc7a6330 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -1136,6 +1136,18 @@ def add_auth(self, request): request.headers['Authorization'] = auth_header +def resolve_auth_type(auth_trait): + for auth_type in auth_trait: + if auth_type == 'smithy.api#noAuth': + return AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] + elif auth_type in AUTH_TYPE_TO_SIGNATURE_VERSION: + signature_version = AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] + if signature_version in AUTH_TYPE_MAPS: + return signature_version + else: + raise UnknownSignatureVersionError(signature_version=auth_type) + + AUTH_TYPE_MAPS = { 'v2': SigV2Auth, 'v3': SigV3Auth, @@ -1171,15 +1183,3 @@ def add_auth(self, request): 'smithy.api#httpBearerAuth': 'bearer', 'smithy.api#noAuth': 'none', } - - -def resolve_auth_type(auth_trait): - for auth_type in auth_trait: - if auth_type == 'smithy.api#noAuth': - return AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] - elif auth_type in AUTH_TYPE_TO_SIGNATURE_VERSION: - signature_version = AUTH_TYPE_TO_SIGNATURE_VERSION[auth_type] - if signature_version in AUTH_TYPE_MAPS: - return signature_version - else: - raise UnknownSignatureVersionError(signature_version=auth_type) diff --git a/botocore/client.py b/botocore/client.py index 999b00e8e1..340988a5cb 100644 --- a/botocore/client.py +++ b/botocore/client.py @@ -958,12 +958,10 @@ def _make_api_call(self, operation_name, api_params): 'client_region': self.meta.region_name, 'client_config': self.meta.config, 'has_streaming_input': operation_model.has_streaming_input, - 'auth_type': operation_model.auth_type, + 'auth_type': operation_model.resolved_auth_type, + 'unsigned_payload': operation_model.unsigned_payload, } - if operation_model.unsigned_payload: - request_context['payload_signing_enabled'] = False - api_params = self._emit_api_params( api_params=api_params, operation_model=operation_model, diff --git a/botocore/handlers.py b/botocore/handlers.py index b371aa5b78..99eed3bfc5 100644 --- a/botocore/handlers.py +++ b/botocore/handlers.py @@ -203,6 +203,11 @@ def set_operation_specific_signer(context, signing_name, **kwargs): if auth_type == 'bearer': return 'bearer' + # If the operation needs an unsigned body, we set additional context + # allowing the signer to be aware of this. + if context.get('unsigned_payload') or auth_type == 'v4-unsigned-body': + context['payload_signing_enabled'] = False + if auth_type.startswith('v4'): if auth_type == 'v4-s3express': return auth_type @@ -220,11 +225,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): else: signature_version = 'v4' - # If the operation needs an unsigned body, we set additional context - # allowing the signer to be aware of this. - if auth_type == 'v4-unsigned-body': - context['payload_signing_enabled'] = False - # Signing names used by s3 and s3-control use customized signers "s3v4" # and "s3v4a". if signing_name in S3_SIGNING_NAMES: diff --git a/botocore/model.py b/botocore/model.py index 873b547962..677266c8d2 100644 --- a/botocore/model.py +++ b/botocore/model.py @@ -630,9 +630,13 @@ def auth(self): @CachedProperty def auth_type(self): + return self._operation_model.get('authtype') + + @CachedProperty + def resolved_auth_type(self): if self.auth: return resolve_auth_type(self.auth) - return self._operation_model.get('authtype') + return self.auth_type @CachedProperty def unsigned_payload(self): diff --git a/tests/functional/test_auth_config.py b/tests/functional/test_auth_config.py index f36798e153..8c367c1c3f 100644 --- a/tests/functional/test_auth_config.py +++ b/tests/functional/test_auth_config.py @@ -18,11 +18,15 @@ # signature may fail and others may succeed. e.g. a service may want to use bearer # auth but fall back to sigv4 if a token isn't available. There's currently no way to do # this in botocore, so this test ensures we handle this gracefully when the need arises. + + +# The dictionary's value here needs to be hashable to be added to the set below; any +# new auth types with multiple requirements should be added in a comma-separated list AUTH_TYPE_REQUIREMENTS = { - 'aws.auth#sigv4': ['credentials'], - 'aws.auth#sigv4a': ['credentials'], - 'smithy.api#httpBearerAuth': ['bearer_token'], - 'smithy.api#noAuth': [], + 'aws.auth#sigv4': 'credentials', + 'aws.auth#sigv4a': 'credentials', + 'smithy.api#httpBearerAuth': 'bearer_token', + 'smithy.api#noAuth': 'none', } @@ -67,10 +71,7 @@ def test_all_requirements_match_for_operation(auth_service, operation_model): def assert_all_requirements_match(auth_config, message): - if len(auth_config) > 1: - first_auth = auth_config.pop() - first_auth_reqs = AUTH_TYPE_REQUIREMENTS[first_auth] - assert all( - first_auth_reqs == AUTH_TYPE_REQUIREMENTS[req] - for req in auth_config - ), message + auth_requirements = set() + for auth_type in auth_config: + auth_requirements.add(AUTH_TYPE_REQUIREMENTS[auth_type]) + assert len(auth_requirements) == 1 diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py index 97aa43b046..b493621dda 100644 --- a/tests/unit/auth/test_auth_trait.py +++ b/tests/unit/auth/test_auth_trait.py @@ -11,12 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from botocore.auth import ( - AUTH_TYPE_MAPS, - AUTH_TYPE_TO_SIGNATURE_VERSION, - BaseSigner, - resolve_auth_type, -) +from botocore.auth import BaseSigner, resolve_auth_type from botocore.exceptions import UnknownSignatureVersionError from tests import mock, unittest diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index ad7143a521..86974fd553 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1713,17 +1713,6 @@ def test_client_internal_credential_shim(self): ) self.assertEqual(service_client._get_credentials(), self.credentials) - def test_unsigned_payload_disables_signing(self): - creator = self.create_client_creator() - service_client = creator.create_client('myservice', 'us-west-2') - test_operation = self.service_description['operations'][ - 'TestOperation' - ] - test_operation['unsignedPayload'] = True - service_client.test_operation(Foo='one') - context = self.endpoint.make_request.call_args[0][1]['context'] - self.assertFalse(context['payload_signing_enabled']) - class TestClientErrors(TestAutoGeneratedClient): def add_error_response(self, error_response): diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py index 1e3cac1411..fef4c43eab 100644 --- a/tests/unit/test_handlers.py +++ b/tests/unit/test_handlers.py @@ -1036,13 +1036,11 @@ def test_set_operation_specific_signer_v4a_existing_signing_context(self): signing_name = 'myservice' context = { 'auth_type': 'v4a', - 'signing': {'foo': 'bar', 'region': 'abc'}, + 'signing': {'foo': 'bar'}, } handlers.set_operation_specific_signer( context=context, signing_name=signing_name ) - # region has been updated - self.assertEqual(context['signing']['region'], 'abc') # signing_name has been added self.assertEqual(context['signing']['signing_name'], signing_name) # foo remained untouched @@ -1090,12 +1088,44 @@ def test_set_operation_specific_signer_prefers_client_config(self): 'client_config': Config( sigv4a_signing_region_set="region_1,region_2" ), + 'signing': { + 'region': 'abc', + }, } handlers.set_operation_specific_signer( context=context, signing_name=signing_name ) self.assertEqual(context['signing']['region'], 'region_1,region_2') + def test_payload_signing_disabled_sets_proper_key(self): + signing_name = 'myservice' + context = { + 'auth_type': 'v4', + 'signing': { + 'foo': 'bar', + 'region': 'abc', + }, + 'unsigned_payload': True, + } + handlers.set_operation_specific_signer( + context=context, signing_name=signing_name + ) + self.assertEqual(context.get('payload_signing_enabled'), False) + + def test_no_payload_signing_disabled_does_not_set_key(self): + signing_name = 'myservice' + context = { + 'auth_type': 'v4', + 'signing': { + 'foo': 'bar', + 'region': 'abc', + }, + } + handlers.set_operation_specific_signer( + context=context, signing_name=signing_name + ) + self.assertNotIn('payload_signing_enabled', context) + @pytest.mark.parametrize( 'auth_type, expected_response', [('v4', 's3v4'), ('v4a', 's3v4a')] diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py index 303ed1a31a..da95a18bea 100644 --- a/tests/unit/test_model.py +++ b/tests/unit/test_model.py @@ -182,6 +182,7 @@ def setUp(self): 'errors': [{'shape': 'NoSuchResourceException'}], 'documentation': 'Docs for OperationName', 'authtype': 'v4', + 'auth': ['aws.auth#sigv4'], }, 'OperationTwo': { 'http': { @@ -396,6 +397,22 @@ def test_error_shapes(self): operation.error_shapes[0].name, 'NoSuchResourceException' ) + def test_has_auth(self): + operation = self.service_model.operation_model('OperationName') + self.assertEqual(operation.auth, ["aws.auth#sigv4"]) + + def test_auth_not_set(self): + operation = self.service_model.operation_model('OperationTwo') + self.assertIsNone(operation.auth) + + def test_has_resolved_auth_type(self): + operation = self.service_model.operation_model('OperationName') + self.assertEqual(operation.resolved_auth_type, 'v4') + + def test_resolved_auth_type_not_set(self): + operation = self.service_model.operation_model('OperationTwo') + self.assertIsNone(operation.resolved_auth_type) + def test_has_auth_type(self): operation = self.service_model.operation_model('OperationName') self.assertEqual(operation.auth_type, 'v4') From 35081e9730ded783a50791fe952d0dd3a4baab05 Mon Sep 17 00:00:00 2001 From: SamRemis Date: Mon, 12 Aug 2024 18:08:05 -0400 Subject: [PATCH 06/10] Update tests/functional/test_auth_config.py Change test to use a comprehension instead of a for loop Co-authored-by: Nate Prewitt --- tests/functional/test_auth_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_auth_config.py b/tests/functional/test_auth_config.py index 8c367c1c3f..7fe096d338 100644 --- a/tests/functional/test_auth_config.py +++ b/tests/functional/test_auth_config.py @@ -71,7 +71,7 @@ def test_all_requirements_match_for_operation(auth_service, operation_model): def assert_all_requirements_match(auth_config, message): - auth_requirements = set() - for auth_type in auth_config: - auth_requirements.add(AUTH_TYPE_REQUIREMENTS[auth_type]) + auth_requirements = set( + AUTH_TYPE_REQUIREMENTS[auth_type] for auth_type in auth_config + ) assert len(auth_requirements) == 1 From a06ff3b79c22b52c26f5797f648c80a297026f6a Mon Sep 17 00:00:00 2001 From: SamRemis Date: Mon, 12 Aug 2024 18:48:57 -0400 Subject: [PATCH 07/10] Add error for no supported signature version in auth trait --- botocore/auth.py | 2 ++ botocore/exceptions.py | 10 ++++++++++ tests/unit/auth/test_auth_trait.py | 12 +++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/botocore/auth.py b/botocore/auth.py index 70dc7a6330..42e36822a0 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -38,6 +38,7 @@ from botocore.exceptions import ( NoAuthTokenError, NoCredentialsError, + NoSupportedSignatureVersionError, UnknownSignatureVersionError, ) from botocore.utils import ( @@ -1146,6 +1147,7 @@ def resolve_auth_type(auth_trait): return signature_version else: raise UnknownSignatureVersionError(signature_version=auth_type) + raise NoSupportedSignatureVersionError(auth=auth_trait) AUTH_TYPE_MAPS = { diff --git a/botocore/exceptions.py b/botocore/exceptions.py index 1c480abbf8..528a4f6c97 100644 --- a/botocore/exceptions.py +++ b/botocore/exceptions.py @@ -219,6 +219,16 @@ class UnknownSignatureVersionError(BotoCoreError): fmt = 'Unknown Signature Version: {signature_version}.' +class NoSupportedSignatureVersionError(BotoCoreError): + """ + None of the requested signature versions are supported by this version of botocore + + :ivar auth: The list of requested signature versions. + """ + + fmt = 'No supported signatures in requested versions: {auth}.' + + class ServiceNotInRegionError(BotoCoreError): """ The service is not available in requested region. diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py index b493621dda..6baa080b34 100644 --- a/tests/unit/auth/test_auth_trait.py +++ b/tests/unit/auth/test_auth_trait.py @@ -12,7 +12,10 @@ # language governing permissions and limitations under the License. from botocore.auth import BaseSigner, resolve_auth_type -from botocore.exceptions import UnknownSignatureVersionError +from botocore.exceptions import ( + NoSupportedSignatureVersionError, + UnknownSignatureVersionError, +) from tests import mock, unittest @@ -31,6 +34,9 @@ def test_auth_resolves_first_available(self): assert resolve_auth_type(auth) == 'bar' def test_invalid_auth_type_error(self): - auth = ['aws.auth#invalidAuth'] with self.assertRaises(UnknownSignatureVersionError): - resolve_auth_type(auth) + resolve_auth_type(['aws.auth#invalidAuth']) + + def test_no_known_auth_type(self): + with self.assertRaises(NoSupportedSignatureVersionError): + resolve_auth_type([]) From fab905730df1a8e971829eb0faa58b20f0377eba Mon Sep 17 00:00:00 2001 From: SamRemis Date: Tue, 13 Aug 2024 14:15:25 -0400 Subject: [PATCH 08/10] Update error thrown --- botocore/auth.py | 4 ++-- botocore/exceptions.py | 12 +----------- tests/unit/auth/test_auth_trait.py | 4 ++-- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/botocore/auth.py b/botocore/auth.py index 42e36822a0..66e605a665 100644 --- a/botocore/auth.py +++ b/botocore/auth.py @@ -38,8 +38,8 @@ from botocore.exceptions import ( NoAuthTokenError, NoCredentialsError, - NoSupportedSignatureVersionError, UnknownSignatureVersionError, + UnsupportedSignatureVersionError, ) from botocore.utils import ( is_valid_ipv6_endpoint_url, @@ -1147,7 +1147,7 @@ def resolve_auth_type(auth_trait): return signature_version else: raise UnknownSignatureVersionError(signature_version=auth_type) - raise NoSupportedSignatureVersionError(auth=auth_trait) + raise UnsupportedSignatureVersionError(signature_version=auth_trait) AUTH_TYPE_MAPS = { diff --git a/botocore/exceptions.py b/botocore/exceptions.py index 528a4f6c97..9fa0dfaa84 100644 --- a/botocore/exceptions.py +++ b/botocore/exceptions.py @@ -219,16 +219,6 @@ class UnknownSignatureVersionError(BotoCoreError): fmt = 'Unknown Signature Version: {signature_version}.' -class NoSupportedSignatureVersionError(BotoCoreError): - """ - None of the requested signature versions are supported by this version of botocore - - :ivar auth: The list of requested signature versions. - """ - - fmt = 'No supported signatures in requested versions: {auth}.' - - class ServiceNotInRegionError(BotoCoreError): """ The service is not available in requested region. @@ -524,7 +514,7 @@ class UnknownClientMethodError(BotoCoreError): class UnsupportedSignatureVersionError(BotoCoreError): """Error when trying to use an unsupported Signature Version.""" - fmt = 'Signature version is not supported: {signature_version}' + fmt = 'Signature version(s) are not supported: {signature_version}' class ClientError(Exception): diff --git a/tests/unit/auth/test_auth_trait.py b/tests/unit/auth/test_auth_trait.py index 6baa080b34..c1209a576c 100644 --- a/tests/unit/auth/test_auth_trait.py +++ b/tests/unit/auth/test_auth_trait.py @@ -13,8 +13,8 @@ from botocore.auth import BaseSigner, resolve_auth_type from botocore.exceptions import ( - NoSupportedSignatureVersionError, UnknownSignatureVersionError, + UnsupportedSignatureVersionError, ) from tests import mock, unittest @@ -38,5 +38,5 @@ def test_invalid_auth_type_error(self): resolve_auth_type(['aws.auth#invalidAuth']) def test_no_known_auth_type(self): - with self.assertRaises(NoSupportedSignatureVersionError): + with self.assertRaises(UnsupportedSignatureVersionError): resolve_auth_type([]) From 639ebb0b048c6d290c58912a609cd4cc02a82019 Mon Sep 17 00:00:00 2001 From: SamRemis Date: Wed, 14 Aug 2024 15:49:31 -0400 Subject: [PATCH 09/10] Update .changes/next-release/enhancement-signing-83434.json Co-authored-by: Nate Prewitt --- .changes/next-release/enhancement-signing-83434.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/next-release/enhancement-signing-83434.json b/.changes/next-release/enhancement-signing-83434.json index 6ec6206594..5476683fed 100644 --- a/.changes/next-release/enhancement-signing-83434.json +++ b/.changes/next-release/enhancement-signing-83434.json @@ -1,5 +1,5 @@ { - "type": "enhancement", + "type": "feature", "category": "signing", "description": "Adds support for the new 'auth' trait on our models that will allow a priority list of auth types for a service or operation." } From 12995d321ed72c787a2155eceb5eebf6d1ad28a2 Mon Sep 17 00:00:00 2001 From: SamRemis Date: Wed, 14 Aug 2024 16:20:44 -0400 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: Nate Prewitt --- .changes/next-release/enhancement-signing-83434.json | 2 +- botocore/client.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.changes/next-release/enhancement-signing-83434.json b/.changes/next-release/enhancement-signing-83434.json index 5476683fed..a90654a772 100644 --- a/.changes/next-release/enhancement-signing-83434.json +++ b/.changes/next-release/enhancement-signing-83434.json @@ -1,5 +1,5 @@ { "type": "feature", "category": "signing", - "description": "Adds support for the new 'auth' trait on our models that will allow a priority list of auth types for a service or operation." + "description": "Adds internal support for the new 'auth' trait to allow a priority list of auth types for a service or operation." } diff --git a/botocore/client.py b/botocore/client.py index 340988a5cb..ab1be75365 100644 --- a/botocore/client.py +++ b/botocore/client.py @@ -148,8 +148,7 @@ def create_client( region_name, client_config = self._normalize_fips_region( region_name, client_config ) - auth = service_model.metadata.get('auth') - if auth: + if auth := service_model.metadata.get('auth'): service_signature_version = resolve_auth_type(auth) else: service_signature_version = service_model.metadata.get(