diff --git a/.changes/next-release/bugfix-signing-24412.json b/.changes/next-release/bugfix-signing-24412.json new file mode 100644 index 000000000000..33669211567b --- /dev/null +++ b/.changes/next-release/bugfix-signing-24412.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "signing", + "description": "Fix bug so that configured auth scheme preference is used when auth scheme is resolved from endpoints rulesets, or from operation-level auth trait. Auth scheme preference can be configured using the existing ``auth_scheme_preference`` shared config setting, or the existing ``AWS_AUTH_SCHEME_PREFERENCE`` environment variable." +} diff --git a/awscli/botocore/args.py b/awscli/botocore/args.py index 522a4ee4189e..b3cc575bd580 100644 --- a/awscli/botocore/args.py +++ b/awscli/botocore/args.py @@ -119,6 +119,7 @@ def get_client_args( s3_disable_express_session_auth = config_kwargs[ 's3_disable_express_session_auth' ] + preferred_auth_schemes = config_kwargs['auth_scheme_preference'] event_emitter = copy.copy(self._event_emitter) signer = RequestSigner( @@ -169,6 +170,7 @@ def get_client_args( credentials, account_id_endpoint_mode, s3_disable_express_session_auth, + preferred_auth_schemes, ) # Copy the session's user agent factory and adds client configuration. @@ -589,6 +591,7 @@ def _build_endpoint_resolver( credentials, account_id_endpoint_mode, s3_disable_express_session_auth, + preferred_auth_schemes, ): if endpoints_ruleset_data is None: return None @@ -645,6 +648,7 @@ def _build_endpoint_resolver( event_emitter=event_emitter, use_ssl=is_secure, requested_auth_scheme=sig_version, + preferred_auth_schemes=preferred_auth_schemes, ) def compute_endpoint_resolver_builtin_defaults( diff --git a/awscli/botocore/client.py b/awscli/botocore/client.py index b04b3f5c70c5..d41e0bafdb5a 100644 --- a/awscli/botocore/client.py +++ b/awscli/botocore/client.py @@ -19,7 +19,11 @@ xform_name, ) from botocore.args import ClientArgsCreator -from botocore.auth import AUTH_TYPE_MAPS, resolve_auth_type +from botocore.auth import ( + AUTH_TYPE_MAPS, + resolve_auth_scheme_preference, + resolve_auth_type, +) from botocore.awsrequest import prepare_request_dict from botocore.compress import maybe_compress_request @@ -838,11 +842,23 @@ def _make_api_call(self, operation_name, api_params): logger.debug( 'Warning: %s.%s() is deprecated', service_name, operation_name ) + # If the operation has the `auth` property and the client has a + # configured auth scheme preference, use both to compute the + # auth type. Otherwise, fallback to auth/auth_type resolution. + if operation_model.auth and self.meta.config.auth_scheme_preference: + preferred_schemes = ( + self.meta.config.auth_scheme_preference.split(',') + ) + auth_type = resolve_auth_scheme_preference( + preferred_schemes, operation_model.auth + ) + else: + auth_type = operation_model.resolved_auth_type request_context = { 'client_region': self.meta.region_name, 'client_config': self.meta.config, 'has_streaming_input': operation_model.has_streaming_input, - 'auth_type': operation_model.resolved_auth_type, + 'auth_type': auth_type, 'unsigned_payload': operation_model.unsigned_payload, 'auth_options': self._service_model.metadata.get('auth'), } diff --git a/awscli/botocore/regions.py b/awscli/botocore/regions.py index 4ed4145b6c88..fc004163c0fc 100644 --- a/awscli/botocore/regions.py +++ b/awscli/botocore/regions.py @@ -471,6 +471,7 @@ def __init__( event_emitter, use_ssl=True, requested_auth_scheme=None, + preferred_auth_schemes=None, ): self._provider = EndpointProvider( ruleset_data=endpoint_ruleset_data, @@ -483,6 +484,7 @@ def __init__( self._event_emitter = event_emitter self._use_ssl = use_ssl self._requested_auth_scheme = requested_auth_scheme + self._preferred_auth_schemes = preferred_auth_schemes self._instance_cache = {} def construct_endpoint( @@ -698,6 +700,17 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): if self._requested_auth_scheme == UNSIGNED: return 'none', {} + # if a preferred auth schemes list is provided, reorder the auth schemes + # list based on the preferred ordering. + if self._preferred_auth_schemes is not None: + prefs = self._preferred_auth_schemes.split(',') + by_name = {s['name']: s for s in auth_schemes} + auth_schemes = [ + by_name[p] for p in prefs if p in by_name + ] + [ + s for s in auth_schemes if s['name'] not in prefs + ] + auth_schemes = [ {**scheme, 'name': self._strip_sig_prefix(scheme['name'])} for scheme in auth_schemes diff --git a/tests/unit/botocore/test_endpoint_provider.py b/tests/unit/botocore/test_endpoint_provider.py index 747264aabb8f..41010c7c8392 100644 --- a/tests/unit/botocore/test_endpoint_provider.py +++ b/tests/unit/botocore/test_endpoint_provider.py @@ -80,6 +80,28 @@ ], }, } +ENDPOINT_AUTH_SCHEMES_DICT = { + "url": URL_TEMPLATE, + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": True, + "name": "foo", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": True, + "name": "bar", + "signingName": "s3-outposts", + "signingRegion": REGION_TEMPLATE, + }, + ], + }, + "headers": {}, +} @pytest.fixture(scope="module") @@ -562,4 +584,71 @@ def test_construct_endpoint_parametrized( resolver, '_get_provider_params', return_value=provider_params ): result = resolver.construct_endpoint(None, None, None) - assert result.url == expected_url \ No newline at end of file + assert result.url == expected_url + + +@pytest.mark.parametrize( + "preferred_auth_schemes,expected_auth_scheme_name", + [ + ( + 'foo,bar', + 'foo', + ), + ( + 'bar,foo', + 'bar', + ), + ( + 'xyz,foo,bar', + 'foo', + ), + ], +) +def test_auth_scheme_preference( + preferred_auth_schemes, + expected_auth_scheme_name, + monkeypatch +): + conditions = [ + PARSE_ARN_FUNC, + { + "fn": "not", + "argv": [STRING_EQUALS_FUNC], + }, + { + "fn": "aws.partition", + "argv": [REGION_REF], + "assign": "PartitionResults", + }, + ], + resolver = EndpointRulesetResolver( + endpoint_ruleset_data={ + 'version': '1.0', + 'parameters': {}, + 'rules': [ + { + 'conditions': conditions, + 'type': 'endpoint', + 'endpoint': ENDPOINT_AUTH_SCHEMES_DICT, + } + ], + }, + partition_data={}, + service_model=None, + builtins={}, + client_context=None, + event_emitter=None, + use_ssl=True, + requested_auth_scheme=None, + preferred_auth_schemes=preferred_auth_schemes, + ) + monkeypatch.setattr( + 'botocore.regions.AUTH_TYPE_MAPS', + {'bar': None, 'foo': None} + ) + auth_schemes = [ + {'name': 'foo', 'signingName': 's3', 'signingRegion': 'ap-south-1'}, + {'name': 'bar', 'signingName': 's3', 'signingRegion': 'ap-south-2'}, + ] + name, scheme = resolver.auth_schemes_to_signing_ctx(auth_schemes) + assert name == expected_auth_scheme_name