# Copyright 2015 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 base64 import datetime import re import pytest import botocore.session from botocore import UNSIGNED from botocore.compat import get_md5, parse_qs, urlsplit from botocore.config import Config from botocore.exceptions import ( ClientError, InvalidS3UsEast1RegionalEndpointConfigError, ParamValidationError, UnsupportedS3AccesspointConfigurationError, UnsupportedS3ConfigurationError, ) from botocore.parsers import ResponseParserError from tests import ( BaseSessionTest, ClientHTTPStubber, FreezeTime, create_session, mock, requires_crt, temporary_file, unittest, ) DATE = datetime.datetime(2021, 8, 27, 0, 0, 0) class TestS3BucketValidation(unittest.TestCase): def test_invalid_bucket_name_raises_error(self): session = botocore.session.get_session() s3 = session.create_client("s3") with self.assertRaises(ParamValidationError): s3.put_object( Bucket="adfgasdfadfs/bucket/name", Key="foo", Body=b"asdf" ) class BaseS3OperationTest(BaseSessionTest): def setUp(self): super().setUp() self.region = "us-west-2" self.client = self.session.create_client("s3", self.region) self.http_stubber = ClientHTTPStubber(self.client) class BaseS3ClientConfigurationTest(BaseSessionTest): _V4_AUTH_REGEX = re.compile( r"AWS4-HMAC-SHA256 " r"Credential=\w+/\d+/" r"(?P[a-z0-9-]+)/" r"(?P[a-z0-9-]+)/" ) def setUp(self): super().setUp() self.region = "us-west-2" def assert_signing_region(self, request, expected_region): auth_header = request.headers["Authorization"].decode("utf-8") actual_region = None match = self._V4_AUTH_REGEX.match(auth_header) if match: actual_region = match.group("signing_region") self.assertEqual(expected_region, actual_region) def assert_signing_name(self, request, expected_name): auth_header = request.headers["Authorization"].decode("utf-8") actual_name = None match = self._V4_AUTH_REGEX.match(auth_header) if match: actual_name = match.group("signing_name") self.assertEqual(expected_name, actual_name) def assert_signing_region_in_url(self, url, expected_region): qs_components = parse_qs(urlsplit(url).query) self.assertIn(expected_region, qs_components["X-Amz-Credential"][0]) def assert_endpoint(self, request, expected_endpoint): actual_endpoint = urlsplit(request.url).netloc self.assertEqual(actual_endpoint, expected_endpoint) def create_s3_client(self, **kwargs): client_kwargs = {"region_name": self.region} client_kwargs.update(kwargs) return self.session.create_client("s3", **client_kwargs) def set_config_file(self, fileobj, contents): fileobj.write(contents) fileobj.flush() self.environ["AWS_CONFIG_FILE"] = fileobj.name class TestS3ClientConfigResolution(BaseS3ClientConfigurationTest): def test_no_s3_config(self): client = self.create_s3_client() self.assertIsNone(client.meta.config.s3) def test_client_s3_dualstack_handles_uppercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_dualstack_endpoint = True" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["use_dualstack_endpoint"], True ) def test_client_s3_dualstack_handles_lowercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_dualstack_endpoint = true" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["use_dualstack_endpoint"], True ) def test_client_s3_accelerate_handles_uppercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_accelerate_endpoint = True" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["use_accelerate_endpoint"], True ) def test_client_s3_accelerate_handles_lowercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_accelerate_endpoint = true" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["use_accelerate_endpoint"], True ) def test_client_payload_signing_enabled_handles_uppercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " payload_signing_enabled = True" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["payload_signing_enabled"], True ) def test_client_payload_signing_enabled_handles_lowercase_true(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " payload_signing_enabled = true" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["payload_signing_enabled"], True ) def test_includes_unmodeled_s3_config_vars(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " unmodeled = unmodeled_val" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3["unmodeled"], "unmodeled_val" ) def test_mixed_modeled_and_unmodeled_config_vars(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " payload_signing_enabled = true\n" " unmodeled = unmodeled_val", ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "payload_signing_enabled": True, "unmodeled": "unmodeled_val", }, ) def test_use_arn_region(self): self.environ["AWS_S3_USE_ARN_REGION"] = "true" client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "use_arn_region": True, }, ) def test_use_arn_region_config_var(self): with temporary_file("w") as f: self.set_config_file(f, "[default]\n" "s3_use_arn_region = true") client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "use_arn_region": True, }, ) def test_use_arn_region_nested_config_var(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_arn_region = true" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "use_arn_region": True, }, ) def test_use_arn_region_is_case_insensitive(self): self.environ["AWS_S3_USE_ARN_REGION"] = "True" client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "use_arn_region": True, }, ) def test_use_arn_region_env_var_overrides_config_var(self): self.environ["AWS_S3_USE_ARN_REGION"] = "false" with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_arn_region = true" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "use_arn_region": False, }, ) def test_client_config_use_arn_region_overrides_env_var(self): self.environ["AWS_S3_USE_ARN_REGION"] = "true" client = self.create_s3_client( config=Config(s3={"use_arn_region": False}) ) self.assertEqual( client.meta.config.s3, { "use_arn_region": False, }, ) def test_client_config_use_arn_region_overrides_config_var(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " use_arn_region = true" ) client = self.create_s3_client( config=Config(s3={"use_arn_region": False}) ) self.assertEqual( client.meta.config.s3, { "use_arn_region": False, }, ) def test_us_east_1_regional_env_var(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "regional", }, ) def test_us_east_1_regional_config_var(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3_us_east_1_regional_endpoint = regional" ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "regional", }, ) def test_us_east_1_regional_nested_config_var(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " us_east_1_regional_endpoint = regional", ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "regional", }, ) def test_us_east_1_regional_env_var_overrides_config_var(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " us_east_1_regional_endpoint = legacy", ) client = self.create_s3_client() self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "regional", }, ) def test_client_config_us_east_1_regional_overrides_env_var(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" client = self.create_s3_client( config=Config(s3={"us_east_1_regional_endpoint": "legacy"}) ) self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "legacy", }, ) def test_client_config_us_east_1_regional_overrides_config_var(self): with temporary_file("w") as f: self.set_config_file( f, "[default]\n" "s3 = \n" " us_east_1_regional_endpoint = legacy", ) client = self.create_s3_client( config=Config(s3={"us_east_1_regional_endpoint": "regional"}) ) self.assertEqual( client.meta.config.s3, { "us_east_1_regional_endpoint": "regional", }, ) def test_client_validates_us_east_1_regional(self): with self.assertRaises(InvalidS3UsEast1RegionalEndpointConfigError): self.create_s3_client( config=Config(s3={"us_east_1_regional_endpoint": "not-valid"}) ) def test_client_region_defaults_to_us_east_1(self): client = self.create_s3_client(region_name=None) self.assertEqual(client.meta.region_name, "us-east-1") def test_client_region_remains_us_east_1(self): client = self.create_s3_client(region_name="us-east-1") self.assertEqual(client.meta.region_name, "us-east-1") def test_client_region_remains_aws_global(self): client = self.create_s3_client(region_name="aws-global") self.assertEqual(client.meta.region_name, "aws-global") def test_client_region_defaults_to_aws_global_for_regional(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" client = self.create_s3_client(region_name=None) self.assertEqual(client.meta.region_name, "aws-global") def test_client_region_remains_us_east_1_for_regional(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" client = self.create_s3_client(region_name="us-east-1") self.assertEqual(client.meta.region_name, "us-east-1") def test_client_region_remains_aws_global_for_regional(self): self.environ["AWS_S3_US_EAST_1_REGIONAL_ENDPOINT"] = "regional" client = self.create_s3_client(region_name="aws-global") self.assertEqual(client.meta.region_name, "aws-global") class TestS3Copy(BaseS3OperationTest): def create_s3_client(self, **kwargs): client_kwargs = {"region_name": self.region} client_kwargs.update(kwargs) return self.session.create_client("s3", **client_kwargs) def create_stubbed_s3_client(self, **kwargs): client = self.create_s3_client(**kwargs) http_stubber = ClientHTTPStubber(client) http_stubber.start() return client, http_stubber def test_s3_copy_object_with_empty_response(self): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1" ) empty_body = b"" complete_body = ( b'\n\n' b"' b"2020-04-21T21:03:31.000Z" b""s0mEcH3cK5uM"" ) self.http_stubber.add_response(status=200, body=empty_body) self.http_stubber.add_response(status=200, body=complete_body) response = self.client.copy_object( Bucket="bucket", CopySource="other-bucket/test.txt", Key="test.txt", ) # Validate we retried and got second body self.assertEqual(len(self.http_stubber.requests), 2) self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) self.assertTrue("CopyObjectResult" in response) def test_s3_copy_object_with_incomplete_response(self): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1" ) incomplete_body = b'\n\n\n' self.http_stubber.add_response(status=200, body=incomplete_body) with self.assertRaises(ResponseParserError): self.client.copy_object( Bucket="bucket", CopySource="other-bucket/test.txt", Key="test.txt", ) class TestAccesspointArn(BaseS3ClientConfigurationTest): def setUp(self): super().setUp() self.client, self.http_stubber = self.create_stubbed_s3_client() def create_stubbed_s3_client(self, **kwargs): client = self.create_s3_client(**kwargs) http_stubber = ClientHTTPStubber(client) http_stubber.start() return client, http_stubber def assert_expected_copy_source_header( self, http_stubber, expected_copy_source ): request = self.http_stubber.requests[0] self.assertIn("x-amz-copy-source", request.headers) self.assertEqual( request.headers["x-amz-copy-source"], expected_copy_source ) def add_copy_object_response(self, http_stubber): http_stubber.add_response( body=b"" ) def assert_endpoint(self, request, expected_endpoint): actual_endpoint = urlsplit(request.url).netloc self.assertEqual(actual_endpoint, expected_endpoint) def assert_header_matches(self, request, header_key, expected_value): self.assertEqual(request.headers.get(header_key), expected_value) def test_missing_account_id_in_arn(self): accesspoint_arn = "arn:aws:s3:us-west-2::accesspoint:myendpoint" with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=accesspoint_arn) def test_missing_accesspoint_name_in_arn(self): accesspoint_arn = "arn:aws:s3:us-west-2:123456789012:accesspoint" with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=accesspoint_arn) def test_accesspoint_includes_asterisk(self): accesspoint_arn = "arn:aws:s3:us-west-2:123456789012:accesspoint:*" with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=accesspoint_arn) def test_accesspoint_arn_contains_subresources(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint:object" ) with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=accesspoint_arn) def test_accesspoint_arn_with_custom_endpoint(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, http_stubber = self.create_stubbed_s3_client( endpoint_url="https://custom.com" ) http_stubber.add_response() self.client.list_objects(Bucket=accesspoint_arn) expected_endpoint = "myendpoint-123456789012.custom.com" self.assert_endpoint(http_stubber.requests[0], expected_endpoint) def test_accesspoint_arn_with_custom_endpoint_and_dualstack(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, http_stubber = self.create_stubbed_s3_client( endpoint_url="https://custom.com", config=Config(s3={"use_dualstack_endpoint": True}), ) http_stubber.add_response() self.client.list_objects(Bucket=accesspoint_arn) expected_endpoint = "myendpoint-123456789012.custom.com" self.assert_endpoint(http_stubber.requests[0], expected_endpoint) def test_accesspoint_arn_with_s3_accelerate(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( config=Config(s3={"use_accelerate_endpoint": True}) ) with self.assertRaises( botocore.exceptions.UnsupportedS3AccesspointConfigurationError ): self.client.list_objects(Bucket=accesspoint_arn) def test_accesspoint_arn_cross_partition(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="cn-north-1" ) with self.assertRaises( botocore.exceptions.UnsupportedS3AccesspointConfigurationError ): self.client.list_objects(Bucket=accesspoint_arn) def test_accesspoint_arn_cross_partition_use_client_region(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="cn-north-1", config=Config(s3={"use_accelerate_endpoint": True}), ) with self.assertRaises( botocore.exceptions.UnsupportedS3AccesspointConfigurationError ): self.client.list_objects(Bucket=accesspoint_arn) def test_signs_with_arn_region(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1" ) self.http_stubber.add_response() self.client.list_objects(Bucket=accesspoint_arn) self.assert_signing_region(self.http_stubber.requests[0], "us-west-2") def test_signs_with_client_region_when_use_arn_region_false(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1", config=Config(s3={"use_arn_region": False}), ) self.http_stubber.add_response() with self.assertRaises(UnsupportedS3AccesspointConfigurationError): self.client.list_objects(Bucket=accesspoint_arn) def test_presign_signs_with_arn_region(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="us-east-1", config=Config(signature_version="s3v4") ) url = self.client.generate_presigned_url( "get_object", {"Bucket": accesspoint_arn, "Key": "mykey"} ) self.assert_signing_region_in_url(url, "us-west-2") def test_presign_signs_with_client_region_when_use_arn_region_false(self): accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="us-east-1", config=Config( signature_version="s3v4", s3={"use_arn_region": False} ), ) with self.assertRaises(UnsupportedS3AccesspointConfigurationError): self.client.generate_presigned_url( "get_object", {"Bucket": accesspoint_arn, "Key": "mykey"} ) def test_copy_source_str_with_accesspoint_arn(self): copy_source = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint/" "object/myprefix/myobject" ) self.client, self.http_stubber = self.create_stubbed_s3_client() self.add_copy_object_response(self.http_stubber) self.client.copy_object( Bucket="mybucket", Key="mykey", CopySource=copy_source ) self.assert_expected_copy_source_header( self.http_stubber, expected_copy_source=( b"arn%3Aaws%3As3%3Aus-west-2%3A123456789012%3Aaccesspoint%3A" b"myendpoint/object/myprefix/myobject" ), ) def test_copy_source_str_with_accesspoint_arn_and_version_id(self): copy_source = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint/" "object/myprefix/myobject?versionId=myversionid" ) self.client, self.http_stubber = self.create_stubbed_s3_client() self.add_copy_object_response(self.http_stubber) self.client.copy_object( Bucket="mybucket", Key="mykey", CopySource=copy_source ) self.assert_expected_copy_source_header( self.http_stubber, expected_copy_source=( b"arn%3Aaws%3As3%3Aus-west-2%3A123456789012%3Aaccesspoint%3A" b"myendpoint/object/myprefix/myobject?versionId=myversionid" ), ) def test_copy_source_dict_with_accesspoint_arn(self): copy_source = { "Bucket": "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint", "Key": "myprefix/myobject", } self.client, self.http_stubber = self.create_stubbed_s3_client() self.add_copy_object_response(self.http_stubber) self.client.copy_object( Bucket="mybucket", Key="mykey", CopySource=copy_source ) self.assert_expected_copy_source_header( self.http_stubber, expected_copy_source=( b"arn%3Aaws%3As3%3Aus-west-2%3A123456789012%3Aaccesspoint%3A" b"myendpoint/object/myprefix/myobject" ), ) def test_copy_source_dict_with_accesspoint_arn_and_version_id(self): copy_source = { "Bucket": "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint", "Key": "myprefix/myobject", "VersionId": "myversionid", } self.client, self.http_stubber = self.create_stubbed_s3_client() self.add_copy_object_response(self.http_stubber) self.client.copy_object( Bucket="mybucket", Key="mykey", CopySource=copy_source ) self.assert_expected_copy_source_header( self.http_stubber, expected_copy_source=( b"arn%3Aaws%3As3%3Aus-west-2%3A123456789012%3Aaccesspoint%3A" b"myendpoint/object/myprefix/myobject?versionId=myversionid" ), ) def test_basic_outpost_arn(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456:accesspoint:myaccesspoint" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1" ) self.http_stubber.add_response() self.client.list_objects(Bucket=outpost_arn) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-outposts") self.assert_signing_region(request, "us-west-2") expected_endpoint = ( "myaccesspoint-123456789012.op-01234567890123456." "s3-outposts.us-west-2.amazonaws.com" ) self.assert_endpoint(request, expected_endpoint) sha_header = request.headers.get("x-amz-content-sha256") self.assertIsNotNone(sha_header) self.assertNotEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_basic_outpost_arn_custom_endpoint(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456:accesspoint:myaccesspoint" ) self.client, self.http_stubber = self.create_stubbed_s3_client( endpoint_url="https://custom.com", region_name="us-east-1" ) self.http_stubber.add_response() self.client.list_objects(Bucket=outpost_arn) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-outposts") self.assert_signing_region(request, "us-west-2") expected_endpoint = ( "myaccesspoint-123456789012.op-01234567890123456.custom.com" ) self.assert_endpoint(request, expected_endpoint) def test_outpost_arn_presigned_url(self): outpost_arn = ( 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/' 'op-01234567890123456/accesspoint/myaccesspoint' ) expected_url = ( 'myaccesspoint-123456789012.op-01234567890123456.' 's3-outposts.us-west-2.amazonaws.com' ) expected_credentials = ( '20210827%2Fus-west-2%2Fs3-outposts%2Faws4_request' ) expected_signature = ( 'a944fbe2bfbae429f922746546d1c6f890649c88ba7826bd1d258ac13f327e09' ) config = Config(signature_version='s3v4') presigned_url = self._get_presigned_url( outpost_arn, 'us-west-2', config=config ) self._assert_presigned_url( presigned_url, expected_url, expected_signature, expected_credentials, ) def test_outpost_arn_presigned_url_with_use_arn_region(self): outpost_arn = ( 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/' 'op-01234567890123456/accesspoint/myaccesspoint' ) expected_url = ( 'myaccesspoint-123456789012.op-01234567890123456.' 's3-outposts.us-west-2.amazonaws.com' ) expected_credentials = ( '20210827%2Fus-west-2%2Fs3-outposts%2Faws4_request' ) expected_signature = ( 'a944fbe2bfbae429f922746546d1c6f890649c88ba7826bd1d258ac13f327e09' ) config = Config( signature_version='s3v4', s3={ 'use_arn_region': True, }, ) presigned_url = self._get_presigned_url( outpost_arn, 'us-west-2', config=config ) self._assert_presigned_url( presigned_url, expected_url, expected_signature, expected_credentials, ) def test_outpost_arn_presigned_url_cross_region_arn(self): outpost_arn = ( 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/' 'op-01234567890123456/accesspoint/myaccesspoint' ) expected_url = ( 'myaccesspoint-123456789012.op-01234567890123456.' 's3-outposts.us-east-1.amazonaws.com' ) expected_credentials = ( '20210827%2Fus-east-1%2Fs3-outposts%2Faws4_request' ) expected_signature = ( '7f93df0b81f80e590d95442d579bd6cf749a35ff4bbdc6373fa669b89c7fce4e' ) config = Config( signature_version='s3v4', s3={ 'use_arn_region': True, }, ) presigned_url = self._get_presigned_url( outpost_arn, 'us-west-2', config=config ) self._assert_presigned_url( presigned_url, expected_url, expected_signature, expected_credentials, ) def test_outpost_arn_with_s3_accelerate(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456:accesspoint:myaccesspoint" ) self.client, _ = self.create_stubbed_s3_client( config=Config(s3={"use_accelerate_endpoint": True}) ) with self.assertRaises(UnsupportedS3AccesspointConfigurationError): self.client.list_objects(Bucket=outpost_arn) def test_outpost_arn_with_s3_dualstack(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456:accesspoint:myaccesspoint" ) self.client, _ = self.create_stubbed_s3_client( config=Config(s3={"use_dualstack_endpoint": True}) ) with self.assertRaises(UnsupportedS3AccesspointConfigurationError): self.client.list_objects(Bucket=outpost_arn) def test_incorrect_outpost_format(self): outpost_arn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost" with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=outpost_arn) def test_incorrect_outpost_no_accesspoint(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456" ) with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=outpost_arn) def test_incorrect_outpost_resource_format(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:myaccesspoint" ) with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=outpost_arn) def test_incorrect_outpost_sub_resources(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-01234567890123456:accesspoint:mybucket:object:foo" ) with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=outpost_arn) def test_incorrect_outpost_invalid_character(self): outpost_arn = ( "arn:aws:s3-outposts:us-west-2:123456789012:outpost:" "op-0123456.890123456:accesspoint:myaccesspoint" ) with self.assertRaises(botocore.exceptions.ParamValidationError): self.client.list_objects(Bucket=outpost_arn) def test_s3_object_lambda_arn_with_s3_dualstack(self): s3_object_lambda_arn = ( "arn:aws:s3-object-lambda:us-west-2:123456789012:" "accesspoint/myBanner" ) self.client, _ = self.create_stubbed_s3_client( config=Config(s3={"use_dualstack_endpoint": True}) ) with self.assertRaises(UnsupportedS3AccesspointConfigurationError): self.client.list_objects(Bucket=s3_object_lambda_arn) def test_s3_object_lambda_fips_raise_for_cross_region(self): s3_object_lambda_arn = ( "arn:aws-us-gov:s3-object-lambda:us-gov-east-1:123456789012:" "accesspoint/mybanner" ) self.client, _ = self.create_stubbed_s3_client( region_name="fips-us-gov-west-1", config=Config(s3={"use_arn_region": False}), ) expected_exception = UnsupportedS3AccesspointConfigurationError with self.assertRaises(expected_exception): self.client.list_objects(Bucket=s3_object_lambda_arn) def test_s3_object_lambda_with_global_regions(self): s3_object_lambda_arn = ( "arn:aws:s3-object-lambda:us-east-1:123456789012:" "accesspoint/mybanner" ) expected_exception = UnsupportedS3AccesspointConfigurationError for region in ("aws-global", "s3-external-1"): self.client, _ = self.create_stubbed_s3_client( region_name=region, config=Config(s3={"use_arn_region": False}) ) with self.assertRaises(expected_exception): self.client.list_objects(Bucket=s3_object_lambda_arn) def test_s3_object_lambda_arn_with_us_east_1(self): # test that us-east-1 region is not resolved # into s3 global endpoint s3_object_lambda_arn = ( "arn:aws:s3-object-lambda:us-east-1:123456789012:" "accesspoint/myBanner" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1", config=Config(s3={"use_arn_region": False}), ) self.http_stubber.add_response() self.client.list_objects(Bucket=s3_object_lambda_arn) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-object-lambda") self.assert_signing_region(request, "us-east-1") expected_endpoint = ( "myBanner-123456789012.s3-object-lambda.us-east-1.amazonaws.com" ) self.assert_endpoint(request, expected_endpoint) def test_basic_s3_object_lambda_arn(self): s3_object_lambda_arn = ( "arn:aws:s3-object-lambda:us-west-2:123456789012:" "accesspoint/myBanner" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-east-1" ) self.http_stubber.add_response() self.client.list_objects(Bucket=s3_object_lambda_arn) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-object-lambda") self.assert_signing_region(request, "us-west-2") expected_endpoint = ( "myBanner-123456789012.s3-object-lambda.us-west-2.amazonaws.com" ) self.assert_endpoint(request, expected_endpoint) sha_header = request.headers.get("x-amz-content-sha256") self.assertIsNotNone(sha_header) self.assertNotEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_outposts_raise_exception_if_fips_region(self): outpost_arn = ( "arn:aws:s3-outposts:us-gov-east-1:123456789012:outpost:" "op-01234567890123456:accesspoint:myaccesspoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="fips-east-1" ) expected_exception = UnsupportedS3AccesspointConfigurationError with self.assertRaises(expected_exception): self.client.list_objects(Bucket=outpost_arn) def test_accesspoint_fips_raise_for_cross_region(self): s3_accesspoint_arn = ( "arn:aws-us-gov:s3:us-gov-east-1:123456789012:" "accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="fips-us-gov-west-1", config=Config(s3={"use_arn_region": False}), ) expected_exception = UnsupportedS3AccesspointConfigurationError with self.assertRaises(expected_exception): self.client.list_objects(Bucket=s3_accesspoint_arn) def test_accesspoint_fips_raise_if_fips_in_arn(self): s3_accesspoint_arn = ( "arn:aws-us-gov:s3:fips-us-gov-west-1:123456789012:" "accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="fips-us-gov-west-1", ) expected_exception = UnsupportedS3AccesspointConfigurationError with self.assertRaises(expected_exception): self.client.list_objects(Bucket=s3_accesspoint_arn) def test_accesspoint_with_global_regions(self): s3_accesspoint_arn = ( "arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint" ) self.client, _ = self.create_stubbed_s3_client( region_name="aws-global", config=Config(s3={"use_arn_region": False}), ) expected_exception = UnsupportedS3AccesspointConfigurationError with self.assertRaises(expected_exception): self.client.list_objects(Bucket=s3_accesspoint_arn) # It shouldn't raise if use_arn_region is True self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="s3-external-1", config=Config(s3={"use_arn_region": True}), ) self.http_stubber.add_response() self.client.list_objects(Bucket=s3_accesspoint_arn) request = self.http_stubber.requests[0] expected_endpoint = ( "myendpoint-123456789012.s3-accesspoint." "us-east-1.amazonaws.com" ) self.assert_endpoint(request, expected_endpoint) # It shouldn't raise if no use_arn_region is specified since # use_arn_region defaults to True self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="s3-external-1", ) self.http_stubber.add_response() self.client.list_objects(Bucket=s3_accesspoint_arn) request = self.http_stubber.requests[0] expected_endpoint = ( "myendpoint-123456789012.s3-accesspoint." "us-east-1.amazonaws.com" ) self.assert_endpoint(request, expected_endpoint) @requires_crt() def test_mrap_arn_with_client_regions(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" region_tests = [ ( "us-east-1", "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", ), ( "us-west-2", "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", ), ( "aws-global", "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", ), ] for region, expected in region_tests: self._assert_mrap_endpoint(mrap_arn, region, expected) @requires_crt() def test_mrap_arn_with_other_partition(self): mrap_arn = "arn:aws-cn:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" expected = "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com.cn" self._assert_mrap_endpoint(mrap_arn, "cn-north-1", expected) @requires_crt() def test_mrap_arn_with_invalid_s3_configs(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" config_tests = [ ("us-west-2", Config(s3={"use_dualstack_endpoint": True})), ("us-west-2", Config(s3={"use_accelerate_endpoint": True})), ] for region, config in config_tests: self._assert_mrap_config_failure(mrap_arn, region, config=config) @requires_crt() def test_mrap_arn_with_disable_config_enabled(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" config = Config(s3={"s3_disable_multiregion_access_points": True}) for region in ("us-west-2", "aws-global"): self._assert_mrap_config_failure(mrap_arn, region, config) @requires_crt() def test_mrap_arn_with_disable_config_enabled_custom_endpoint(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:myendpoint" config = Config(s3={"s3_disable_multiregion_access_points": True}) self._assert_mrap_config_failure(mrap_arn, "us-west-2", config) @requires_crt() def test_mrap_arn_with_disable_config_disabled(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" config = Config(s3={"s3_disable_multiregion_access_points": False}) expected = "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com" self._assert_mrap_endpoint( mrap_arn, "us-west-2", expected, config=config ) @requires_crt() def test_global_arn_without_mrap_suffix(self): global_arn_tests = [ ( "arn:aws:s3::123456789012:accesspoint:myendpoint", "myendpoint.accesspoint.s3-global.amazonaws.com", ), ( "arn:aws:s3::123456789012:accesspoint:my.bucket", "my.bucket.accesspoint.s3-global.amazonaws.com", ), ] for arn, expected in global_arn_tests: self._assert_mrap_endpoint(arn, "us-west-2", expected) @requires_crt() def test_mrap_signing_algorithm_is_sigv4a(self): s3_accesspoint_arn = ( "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" ) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-west-2" ) self.http_stubber.add_response() self.client.list_objects(Bucket=s3_accesspoint_arn) request = self.http_stubber.requests[0] self._assert_sigv4a_used(request.headers) @requires_crt() def test_mrap_presigned_url(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" config = Config(s3={"s3_disable_multiregion_access_points": False}) expected_url = "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com" self._assert_mrap_presigned_url( mrap_arn, "us-west-2", expected_url, config=config ) @requires_crt() def test_mrap_presigned_url_disabled(self): mrap_arn = "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap" config = Config(s3={"s3_disable_multiregion_access_points": True}) self._assert_mrap_config_presigned_failure( mrap_arn, "us-west-2", config ) def _assert_mrap_config_failure(self, arn, region, config): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name=region, config=config ) with self.assertRaises( botocore.exceptions.UnsupportedS3AccesspointConfigurationError ): self.client.list_objects(Bucket=arn) @FreezeTime(botocore.auth.datetime, date=DATE) def _get_presigned_url(self, arn, region, config=None, endpoint_url=None): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name=region, endpoint_url=endpoint_url, config=config, aws_access_key_id='ACCESS_KEY_ID', aws_secret_access_key='SECRET_ACCESS_KEY', ) presigned_url = self.client.generate_presigned_url( 'get_object', Params={'Bucket': arn, 'Key': 'obj'}, ExpiresIn=900 ) return presigned_url def _assert_presigned_url( self, presigned_url, expected_url, expected_signature, expected_credentials, ): url_parts = urlsplit(presigned_url) assert url_parts.netloc == expected_url query_strs = url_parts.query.split('&') query_parts = dict(part.split('=') for part in query_strs) assert expected_signature == query_parts['X-Amz-Signature'] assert expected_credentials in query_parts['X-Amz-Credential'] def _assert_mrap_presigned_url( self, arn, region, expected, endpoint_url=None, config=None ): presigned_url = self._get_presigned_url( arn, region, endpoint_url=endpoint_url, config=config ) url_parts = urlsplit(presigned_url) self.assertEqual(expected, url_parts.hostname) # X-Amz-Region-Set header MUST be * (percent-encoded as %2A) for MRAPs self.assertIn("X-Amz-Region-Set=%2A", url_parts.query) def _assert_mrap_config_presigned_failure(self, arn, region, config): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name=region, config=config ) with self.assertRaises( botocore.exceptions.UnsupportedS3AccesspointConfigurationError ): self.client.generate_presigned_url( "get_object", Params={"Bucket": arn, "Key": "test_object"} ) def _assert_mrap_endpoint( self, arn, region, expected, endpoint_url=None, config=None ): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name=region, endpoint_url=endpoint_url, config=config ) self.http_stubber.add_response() self.client.list_objects(Bucket=arn) request = self.http_stubber.requests[0] self.assert_endpoint(request, expected) # MRAP requests MUST include a global signing region stored in the # X-Amz-Region-Set header as *. self.assert_header_matches(request, "X-Amz-Region-Set", b"*") def _assert_sigv4a_used(self, headers): self.assertIn( b"AWS4-ECDSA-P256-SHA256", headers.get("Authorization", "") ) class TestOnlyAsciiCharsAllowed(BaseS3OperationTest): def test_validates_non_ascii_chars_trigger_validation_error(self): self.http_stubber.add_response() with self.http_stubber: with self.assertRaises(ParamValidationError): self.client.put_object( Bucket="foo", Key="bar", Metadata={"goodkey": "good", "non-ascii": "\u2713"}, ) class TestS3GetBucketLifecycle(BaseS3OperationTest): def test_multiple_transitions_returns_one(self): response_body = ( b'' b'' b" " b" transitionRule" b" foo" b" Enabled" b" " b" 40" b" STANDARD_IA" b" " b" " b" 70" b" GLACIER" b" " b" " b" " b" noncurrentVersionRule" b" bar" b" Enabled" b" " b" 40" b" STANDARD_IA" b" " b" " b" 70" b" GLACIER" b" " b" " b"" ) s3 = self.session.create_client("s3") with ClientHTTPStubber(s3) as http_stubber: http_stubber.add_response(body=response_body) response = s3.get_bucket_lifecycle(Bucket="mybucket") # Each Transition member should have at least one of the # transitions provided. self.assertEqual( response["Rules"][0]["Transition"], {"Days": 40, "StorageClass": "STANDARD_IA"}, ) self.assertEqual( response["Rules"][1]["NoncurrentVersionTransition"], {"NoncurrentDays": 40, "StorageClass": "STANDARD_IA"}, ) class TestS3PutObject(BaseS3OperationTest): def test_500_error_with_non_xml_body(self): # Note: This exact tesdict may not be applicable from # an integration standpoint if the issue is fixed in the future. # # The issue is that: # S3 returns a 200 response but the received response from urllib3 has # a 500 status code and the headers are in the body of the # the response. Botocore will try to parse out the error body as xml, # but the body is invalid xml because it is full of headers. # So instead of blowing up on an XML parsing error, we # should at least use the 500 status code because that can be # retried. # # We are unsure of what exactly causes the response to be mangled # but we expect it to be how 100 continues are handled. non_xml_content = ( b"x-amz-id-2: foo\r\n" b"x-amz-request-id: bar\n" b"Date: Tue, 06 Oct 2015 03:20:38 GMT\r\n" b'ETag: "a6d856bc171fc6aa1b236680856094e2"\r\n' b"Content-Length: 0\r\n" b"Server: AmazonS3\r\n" ) s3 = self.session.create_client("s3") with ClientHTTPStubber(s3) as http_stubber: http_stubber.add_response(status=500, body=non_xml_content) http_stubber.add_response() response = s3.put_object( Bucket="mybucket", Key="mykey", Body=b"foo" ) # The first response should have been retried even though the xml is # invalid and eventually return the 200 response. self.assertEqual( response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(http_stubber.requests), 2) class TestWriteGetObjectResponse(BaseS3ClientConfigurationTest): def create_stubbed_s3_client(self, **kwargs): client = self.create_s3_client(**kwargs) http_stubber = ClientHTTPStubber(client) http_stubber.start() return client, http_stubber def test_endpoint_redirection(self): regions = ["us-west-2", "us-east-1"] for region in regions: self.client, self.http_stubber = self.create_stubbed_s3_client( region_name=region ) self.http_stubber.add_response() self.client.write_get_object_response( RequestRoute="endpoint-io.a1c1d5c7", RequestToken="SecretToken", ) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-object-lambda") self.assert_signing_region(request, region) expected_endpoint = ( "endpoint-io.a1c1d5c7.s3-object-lambda." "%s.amazonaws.com" % region ) self.assert_endpoint(request, expected_endpoint) def test_endpoint_redirection_fails_with_custom_endpoint(self): self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-west-2", endpoint_url="https://example.com" ) self.http_stubber.add_response() self.client.write_get_object_response( RequestRoute="endpoint-io.a1c1d5c7", RequestToken="SecretToken", ) request = self.http_stubber.requests[0] self.assert_signing_name(request, "s3-object-lambda") self.assert_signing_region(request, "us-west-2") self.assert_endpoint(request, "endpoint-io.a1c1d5c7.example.com") def test_endpoint_redirection_fails_with_accelerate_endpoint(self): config = Config(s3={"use_accelerate_endpoint": True}) self.client, self.http_stubber = self.create_stubbed_s3_client( region_name="us-west-2", config=config, ) self.http_stubber.add_response() with self.assertRaises(UnsupportedS3ConfigurationError): self.client.write_get_object_response( RequestRoute="endpoint-io.a1c1d5c7", RequestToken="SecretToken", ) class TestS3SigV4(BaseS3OperationTest): def setUp(self): super().setUp() self.client = self.session.create_client( "s3", self.region, config=Config(signature_version="s3v4") ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() def get_sent_headers(self): return self.http_stubber.requests[0].headers def test_content_md5_set(self): with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="baz") self.assertIn("content-md5", self.get_sent_headers()) def test_content_md5_set_empty_body(self): with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="") self.assertIn("content-md5", self.get_sent_headers()) def test_content_md5_set_empty_file(self): with self.http_stubber: with temporary_file("rb") as f: assert f.read() == b"" self.client.put_object(Bucket="foo", Key="bar", Body=f) self.assertIn("content-md5", self.get_sent_headers()) def test_content_sha256_set_if_config_value_is_true(self): # By default, put_object() does not include an x-amz-content-sha256 # header because it also includes a `Content-MD5` header. The # `payload_signing_enabled` config overrides this logic and forces the # header. config = Config( signature_version="s3v4", s3={"payload_signing_enabled": True} ) self.client = self.session.create_client( "s3", self.region, config=config ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="baz") sent_headers = self.get_sent_headers() sha_header = sent_headers.get("x-amz-content-sha256") self.assertNotEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_content_sha256_not_set_if_config_value_is_false(self): config = Config( signature_version="s3v4", s3={"payload_signing_enabled": False} ) self.client = self.session.create_client( "s3", self.region, config=config ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="baz") sent_headers = self.get_sent_headers() sha_header = sent_headers.get("x-amz-content-sha256") self.assertEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_content_sha256_set_if_config_value_not_set_put_object(self): # The default behavior matches payload_signing_enabled=False. For # operations where the `Content-MD5` is present this means that # `x-amz-content-sha256` is present but not set. config = Config(signature_version="s3v4") self.client = self.session.create_client( "s3", self.region, config=config ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="baz") sent_headers = self.get_sent_headers() sha_header = sent_headers.get("x-amz-content-sha256") self.assertEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_content_sha256_set_if_config_value_not_set_list_objects(self): # The default behavior matches payload_signing_enabled=False. For # operations where the `Content-MD5` is not present, this means that # `x-amz-content-sha256` is present and set. config = Config(signature_version="s3v4") self.client = self.session.create_client( "s3", self.region, config=config ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() with self.http_stubber: self.client.list_objects(Bucket="foo") sent_headers = self.get_sent_headers() sha_header = sent_headers.get("x-amz-content-sha256") self.assertIsNotNone(sha_header) self.assertNotEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_content_sha256_set_s3_on_outpost(self): # S3 on Outpost bucket names should behave the same way. config = Config(signature_version="s3v4") bucket = ( 'test-accessp-e0000075431d83bebde8xz5w8ijx1qzlbp3i3kuse10--op-s3' ) self.client = self.session.create_client( "s3", self.region, config=config ) self.http_stubber = ClientHTTPStubber(self.client) self.http_stubber.add_response() with self.http_stubber: self.client.list_objects(Bucket=bucket) sent_headers = self.get_sent_headers() sha_header = sent_headers.get("x-amz-content-sha256") self.assertNotEqual(sha_header, b"UNSIGNED-PAYLOAD") def test_content_sha256_set_if_md5_is_unavailable(self): with mock.patch("botocore.compat.MD5_AVAILABLE", False): with mock.patch("botocore.utils.MD5_AVAILABLE", False): with self.http_stubber: self.client.put_object(Bucket="foo", Key="bar", Body="baz") sent_headers = self.get_sent_headers() unsigned = "UNSIGNED-PAYLOAD" self.assertNotEqual(sent_headers["x-amz-content-sha256"], unsigned) self.assertNotIn("content-md5", sent_headers) class TestCanSendIntegerHeaders(BaseSessionTest): def test_int_values_with_sigv4(self): s3 = self.session.create_client( "s3", config=Config(signature_version="s3v4") ) with ClientHTTPStubber(s3) as http_stubber: http_stubber.add_response() s3.upload_part( Bucket="foo", Key="bar", Body=b"foo", UploadId="bar", PartNumber=1, ContentLength=3, ) headers = http_stubber.requests[0].headers # Verify that the request integer value of 3 has been converted to # string '3'. This also means we've made it pass the signer which # expects string values in order to sign properly. self.assertEqual(headers["Content-Length"], b"3") class TestRegionRedirect(BaseS3OperationTest): def setUp(self): super().setUp() self.client = self.session.create_client( "s3", "us-west-2", config=Config( signature_version="s3v4", s3={"addressing_style": "path"}, ), ) self.http_stubber = ClientHTTPStubber(self.client) self.redirect_response = { "status": 301, "headers": {"x-amz-bucket-region": "eu-central-1"}, "body": ( b'\n' b"" b" PermanentRedirect" b" The bucket you are attempting to access must be" b" addressed using the specified endpoint. Please send " b" all future requests to this endpoint." b" " b" foo" b" foo.s3.eu-central-1.amazonaws.com" b"" ), } self.bad_signing_region_response = { "status": 400, "headers": {"x-amz-bucket-region": "eu-central-1"}, "body": ( b'' b"" b" AuthorizationHeaderMalformed" b" the region us-west-2 is wrong; " b"expecting eu-central-1" b" eu-central-1" b" BD9AA1730D454E39" b" " b"" ), } self.success_response = { "status": 200, "headers": {}, "body": ( b'\n' b"" b" foo" b" " b" " b" 1000" b" url" b" false" b"" ), } def test_region_redirect(self): self.http_stubber.add_response(**self.redirect_response) self.http_stubber.add_response(**self.success_response) with self.http_stubber: response = self.client.list_objects(Bucket="foo") self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) self.assertEqual(len(self.http_stubber.requests), 2) initial_url = ( "https://s3.us-west-2.amazonaws.com/foo" "?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[0].url, initial_url) fixed_url = ( "https://s3.eu-central-1.amazonaws.com/foo" "?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[1].url, fixed_url) def test_region_redirect_cache(self): self.http_stubber.add_response(**self.redirect_response) self.http_stubber.add_response(**self.success_response) self.http_stubber.add_response(**self.success_response) with self.http_stubber: first_response = self.client.list_objects(Bucket="foo") second_response = self.client.list_objects(Bucket="foo") self.assertEqual( first_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual( second_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(self.http_stubber.requests), 3) initial_url = ( "https://s3.us-west-2.amazonaws.com/foo" "?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[0].url, initial_url) fixed_url = ( "https://s3.eu-central-1.amazonaws.com/foo" "?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[1].url, fixed_url) self.assertEqual(self.http_stubber.requests[2].url, fixed_url) def test_resign_request_with_region_when_needed(self): # Create a client with no explicit configuration so we can # verify the default behavior. client = self.session.create_client("s3", "us-west-2") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(**self.bad_signing_region_response) http_stubber.add_response(**self.success_response) first_response = client.list_objects(Bucket="foo") self.assertEqual( first_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(http_stubber.requests), 2) initial_url = ( "https://foo.s3.us-west-2.amazonaws.com/" "?encoding-type=url" ) self.assertEqual(http_stubber.requests[0].url, initial_url) fixed_url = ( "https://foo.s3.eu-central-1.amazonaws.com/" "?encoding-type=url" ) self.assertEqual(http_stubber.requests[1].url, fixed_url) def test_resign_request_in_us_east_1(self): region_headers = {"x-amz-bucket-region": "eu-central-1"} # Verify that the default behavior in us-east-1 will redirect client = self.session.create_client("s3", "us-east-1") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(status=400) http_stubber.add_response(status=400, headers=region_headers) http_stubber.add_response(headers=region_headers) http_stubber.add_response() response = client.head_object(Bucket="foo", Key="bar") self.assertEqual( response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(http_stubber.requests), 4) initial_url = "https://foo.s3.amazonaws.com/bar" self.assertEqual(http_stubber.requests[0].url, initial_url) fixed_url = "https://foo.s3.eu-central-1.amazonaws.com/bar" self.assertEqual(http_stubber.requests[-1].url, fixed_url) def test_resign_request_in_us_east_1_fails(self): region_headers = {"x-amz-bucket-region": "eu-central-1"} # Verify that the final 400 response is propagated # back to the user. client = self.session.create_client("s3", "us-east-1") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(status=400) http_stubber.add_response(status=400, headers=region_headers) http_stubber.add_response(headers=region_headers) # The final request still fails with a 400. http_stubber.add_response(status=400) with self.assertRaises(ClientError): client.head_object(Bucket="foo", Key="bar") self.assertEqual(len(http_stubber.requests), 4) def test_no_region_redirect_for_accesspoint(self): self.http_stubber.add_response(**self.redirect_response) accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) with self.http_stubber: try: self.client.list_objects(Bucket=accesspoint_arn) except self.client.exceptions.ClientError as e: self.assertEqual( e.response["Error"]["Code"], "PermanentRedirect" ) else: self.fail("PermanentRedirect error should have been raised") class TestFipsRegionRedirect(BaseS3OperationTest): def setUp(self): super().setUp() self.client = self.session.create_client( "s3", "fips-us-west-2", config=Config(signature_version="s3v4"), ) self.http_stubber = ClientHTTPStubber(self.client) self.redirect_response = { "status": 301, "headers": {"x-amz-bucket-region": "us-west-1"}, "body": ( b'\n' b"" b" PermanentRedirect" b" The bucket you are attempting to access must be" b" addressed using the specified endpoint. Please send " b" all future requests to this endpoint." b" " b" foo" b" foo.s3-fips.us-west-1.amazonaws.com" b"" ), } self.success_response = { "status": 200, "headers": {}, "body": ( b'\n' b"" b" foo" b" " b" " b" 1000" b" url" b" false" b"" ), } self.bad_signing_region_response = { "status": 400, "headers": {"x-amz-bucket-region": "us-west-1"}, "body": ( b'' b"" b" AuthorizationHeaderMalformed" b" the region us-west-2 is wrong; " b"expecting us-west-1" b" us-west-1" b" BD9AA1730D454E39" b" " b"" ), } def test_fips_region_redirect(self): self.http_stubber.add_response(**self.redirect_response) self.http_stubber.add_response(**self.success_response) with self.http_stubber: response = self.client.list_objects(Bucket="foo") self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) self.assertEqual(len(self.http_stubber.requests), 2) initial_url = ( "https://foo.s3-fips.us-west-2.amazonaws.com/?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[0].url, initial_url) fixed_url = ( "https://foo.s3-fips.us-west-1.amazonaws.com/?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[1].url, fixed_url) def test_fips_region_redirect_cache(self): self.http_stubber.add_response(**self.redirect_response) self.http_stubber.add_response(**self.success_response) self.http_stubber.add_response(**self.success_response) with self.http_stubber: first_response = self.client.list_objects(Bucket="foo") second_response = self.client.list_objects(Bucket="foo") self.assertEqual( first_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual( second_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(self.http_stubber.requests), 3) initial_url = ( "https://foo.s3-fips.us-west-2.amazonaws.com/?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[0].url, initial_url) fixed_url = ( "https://foo.s3-fips.us-west-1.amazonaws.com/?encoding-type=url" ) self.assertEqual(self.http_stubber.requests[1].url, fixed_url) self.assertEqual(self.http_stubber.requests[2].url, fixed_url) def test_fips_resign_request_with_region_when_needed(self): # Create a client with no explicit configuration so we can # verify the default behavior. client = self.session.create_client("s3", "fips-us-west-2") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(**self.bad_signing_region_response) http_stubber.add_response(**self.success_response) first_response = client.list_objects(Bucket="foo") self.assertEqual( first_response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(http_stubber.requests), 2) initial_url = ( "https://foo.s3-fips.us-west-2.amazonaws.com/" "?encoding-type=url" ) self.assertEqual(http_stubber.requests[0].url, initial_url) fixed_url = ( "https://foo.s3-fips.us-west-1.amazonaws.com/" "?encoding-type=url" ) self.assertEqual(http_stubber.requests[1].url, fixed_url) def test_fips_resign_request_in_us_east_1(self): region_headers = {"x-amz-bucket-region": "us-east-2"} # Verify that the default behavior in us-east-1 will redirect client = self.session.create_client("s3", "fips-us-east-1") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(status=400) http_stubber.add_response(status=400, headers=region_headers) http_stubber.add_response(headers=region_headers) http_stubber.add_response() response = client.head_object(Bucket="foo", Key="bar") self.assertEqual( response["ResponseMetadata"]["HTTPStatusCode"], 200 ) self.assertEqual(len(http_stubber.requests), 4) initial_url = "https://foo.s3-fips.us-east-1.amazonaws.com/bar" self.assertEqual(http_stubber.requests[0].url, initial_url) fixed_url = "https://foo.s3-fips.us-east-2.amazonaws.com/bar" self.assertEqual(http_stubber.requests[-1].url, fixed_url) def test_fips_resign_request_in_us_east_1_fails(self): region_headers = {"x-amz-bucket-region": "us-east-2"} # Verify that the final 400 response is propagated # back to the user. client = self.session.create_client("s3", "fips-us-east-1") with ClientHTTPStubber(client) as http_stubber: http_stubber.add_response(status=400) http_stubber.add_response(status=400, headers=region_headers) http_stubber.add_response(headers=region_headers) # The final request still fails with a 400. http_stubber.add_response(status=400) with self.assertRaises(ClientError): client.head_object(Bucket="foo", Key="bar") self.assertEqual(len(http_stubber.requests), 4) def test_fips_no_region_redirect_for_accesspoint(self): self.http_stubber.add_response(**self.redirect_response) accesspoint_arn = ( "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" ) with self.http_stubber: try: self.client.list_objects(Bucket=accesspoint_arn) except self.client.exceptions.ClientError as e: self.assertEqual( e.response["Error"]["Code"], "PermanentRedirect" ) else: self.fail("PermanentRedirect error should have been raised") class TestGeneratePresigned(BaseS3OperationTest): def assert_is_v2_presigned_url(self, url): qs_components = parse_qs(urlsplit(url).query) # Assert that it looks like a v2 presigned url by asserting it does # not have a couple of the v4 qs components and assert that it has the # v2 Signature component. self.assertNotIn("X-Amz-Credential", qs_components) self.assertNotIn("X-Amz-Algorithm", qs_components) self.assertIn("Signature", qs_components) 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) def test_default_presign_uses_sigv2(self): url = self.client.generate_presigned_url(ClientMethod="list_buckets") self.assertNotIn("Algorithm=AWS4-HMAC-SHA256", url) def test_sigv4_presign(self): config = Config(signature_version="s3v4") client = self.session.create_client("s3", self.region, config=config) url = client.generate_presigned_url(ClientMethod="list_buckets") self.assertIn("Algorithm=AWS4-HMAC-SHA256", url) def test_sigv2_presign(self): config = Config(signature_version="s3") client = self.session.create_client("s3", self.region, config=config) url = client.generate_presigned_url(ClientMethod="list_buckets") self.assertNotIn("Algorithm=AWS4-HMAC-SHA256", url) def test_uses_sigv4_for_unknown_region(self): client = self.session.create_client("s3", "us-west-88") url = client.generate_presigned_url(ClientMethod="list_buckets") self.assertIn("Algorithm=AWS4-HMAC-SHA256", url) def test_default_presign_sigv4_in_sigv4_only_region(self): client = self.session.create_client("s3", "us-east-2") url = client.generate_presigned_url(ClientMethod="list_buckets") self.assertIn("Algorithm=AWS4-HMAC-SHA256", url) def test_presign_unsigned(self): config = Config(signature_version=botocore.UNSIGNED) client = self.session.create_client("s3", "us-east-2", config=config) url = client.generate_presigned_url(ClientMethod="list_buckets") self.assertEqual("https://s3.amazonaws.com/", url) def test_presign_url_with_ssec(self): config = Config(signature_version="s3") client = self.session.create_client("s3", "us-east-1", config=config) url = client.generate_presigned_url( ClientMethod="get_object", Params={ "Bucket": "mybucket", "Key": "mykey", "SSECustomerKey": "a" * 32, "SSECustomerAlgorithm": "AES256", }, ) # The md5 of the sse-c key will be injected when parameters are # built so it should show up in the presigned url as well. self.assertIn("x-amz-server-side-encryption-customer-key-md5=", url) def test_presign_s3_accelerate(self): config = Config( signature_version=botocore.UNSIGNED, s3={"use_accelerate_endpoint": True}, ) client = self.session.create_client("s3", "us-east-1", config=config) url = client.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": "mybucket", "Key": "mykey"}, ) # The url should be the accelerate endpoint self.assertEqual( "https://mybucket.s3-accelerate.amazonaws.com/mykey", url ) def test_presign_s3_accelerate_fails_with_fips(self): config = Config( signature_version=botocore.UNSIGNED, s3={"use_accelerate_endpoint": True}, ) client = self.session.create_client( "s3", "fips-us-east-1", config=config ) expected_exception = UnsupportedS3ConfigurationError with self.assertRaisesRegex( expected_exception, "Accelerate cannot be used with FIPS" ): client.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": "mybucket", "Key": "mykey"}, ) def test_presign_post_s3_accelerate(self): config = Config( signature_version=botocore.UNSIGNED, s3={"use_accelerate_endpoint": True}, ) client = self.session.create_client("s3", "us-east-1", config=config) parts = client.generate_presigned_post(Bucket="mybucket", Key="mykey") # The url should be the accelerate endpoint expected = { "fields": {"key": "mykey"}, "url": "https://mybucket.s3-accelerate.amazonaws.com/", } self.assertEqual(parts, expected) def test_presign_uses_v2_for_aws_global(self): client = self.session.create_client("s3", "aws-global") url = client.generate_presigned_url( "get_object", {"Bucket": "mybucket", "Key": "mykey"} ) self.assert_is_v2_presigned_url(url) def test_presign_uses_v2_for_default_region_with_us_east_1_regional(self): config = Config(s3={"us_east_1_regional_endpoint": "regional"}) client = self.session.create_client("s3", config=config) url = client.generate_presigned_url( "get_object", {"Bucket": "mybucket", "Key": "mykey"} ) self.assert_is_v2_presigned_url(url) def test_presign_uses_v2_for_aws_global_with_us_east_1_regional(self): config = Config(s3={"us_east_1_regional_endpoint": "regional"}) client = self.session.create_client("s3", "aws-global", config=config) url = client.generate_presigned_url( "get_object", {"Bucket": "mybucket", "Key": "mykey"} ) self.assert_is_v2_presigned_url(url) def test_presign_uses_v2_for_us_east_1(self): client = self.session.create_client("s3", "us-east-1") url = client.generate_presigned_url( "get_object", {"Bucket": "mybucket", "Key": "mykey"} ) self.assert_is_v2_presigned_url(url) def test_presign_uses_v2_for_us_east_1_with_us_east_1_regional(self): config = Config(s3={"us_east_1_regional_endpoint": "regional"}) client = self.session.create_client("s3", "us-east-1", config=config) url = client.generate_presigned_url( "get_object", {"Bucket": "mybucket", "Key": "mykey"} ) self.assert_is_v2_presigned_url(url) CHECKSUM_TEST_CASES = [ ("put_bucket_tagging", {"Bucket": "foo", "Tagging": {"TagSet": []}}), ( "put_bucket_lifecycle", {"Bucket": "foo", "LifecycleConfiguration": {"Rules": []}}, ), ( "put_bucket_lifecycle_configuration", {"Bucket": "foo", "LifecycleConfiguration": {"Rules": []}}, ), ( "put_bucket_cors", {"Bucket": "foo", "CORSConfiguration": {"CORSRules": []}}, ), ( "delete_objects", {"Bucket": "foo", "Delete": {"Objects": [{"Key": "bar"}]}}, ), ( "put_bucket_replication", { "Bucket": "foo", "ReplicationConfiguration": {"Role": "", "Rules": []}, }, ), ("put_bucket_acl", {"Bucket": "foo", "AccessControlPolicy": {}}), ("put_bucket_logging", {"Bucket": "foo", "BucketLoggingStatus": {}}), ( "put_bucket_notification", {"Bucket": "foo", "NotificationConfiguration": {}}, ), ("put_bucket_policy", {"Bucket": "foo", "Policy": ""}), ( "put_bucket_request_payment", {"Bucket": "foo", "RequestPaymentConfiguration": {"Payer": ""}}, ), ( "put_bucket_versioning", {"Bucket": "foo", "VersioningConfiguration": {}}, ), ("put_bucket_website", {"Bucket": "foo", "WebsiteConfiguration": {}}), ( "put_object_acl", {"Bucket": "foo", "Key": "bar", "AccessControlPolicy": {}}, ), ( "put_object_legal_hold", {"Bucket": "foo", "Key": "bar", "LegalHold": {"Status": "ON"}}, ), ( "put_object_retention", { "Bucket": "foo", "Key": "bar", "Retention": {"RetainUntilDate": "2020-11-05"}, }, ), ( "put_object_lock_configuration", {"Bucket": "foo", "ObjectLockConfiguration": {}}, ), ] accesspoint_arn = "arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint" accesspoint_arn_cn = ( "arn:aws-cn:s3:cn-north-1:123456789012:accesspoint:myendpoint" ) accesspoint_arn_gov = ( "arn:aws-us-gov:s3:us-gov-west-1:123456789012:accesspoint:myendpoint" ) accesspoint_cross_region_arn_gov = ( "arn:aws-us-gov:s3:us-gov-east-1:123456789012:accesspoint:myendpoint" ) @pytest.mark.parametrize("operation, operation_kwargs", CHECKSUM_TEST_CASES) def test_checksums_included_in_expected_operations( operation, operation_kwargs ): """Validate expected calls include Content-MD5 header""" environ = {} with mock.patch("os.environ", environ): environ["AWS_ACCESS_KEY_ID"] = "access_key" environ["AWS_SECRET_ACCESS_KEY"] = "secret_key" environ["AWS_CONFIG_FILE"] = "no-exist-foo" session = create_session() session.config_filename = "no-exist-foo" client = session.create_client("s3") with ClientHTTPStubber(client) as stub: stub.add_response() call = getattr(client, operation) call(**operation_kwargs) assert "Content-MD5" in stub.requests[-1].headers def _s3_addressing_test_cases(): # The default behavior for sigv2. DNS compatible buckets yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.us-west-2.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.us-west-1.amazonaws.com/key", ) yield dict( region="us-west-1", bucket="bucket", key="key", signature_version="s3", is_secure=False, expected_url="http://bucket.s3.us-west-1.amazonaws.com/key", ) # Virtual host addressing is independent of signature version. yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.us-west-2.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-1", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.us-west-1.amazonaws.com/key", ) yield dict( region="us-west-1", bucket="bucket", key="key", signature_version="s3v4", is_secure=False, expected_url="http://bucket.s3.us-west-1.amazonaws.com/key", ) yield dict( region="us-west-1", bucket="bucket-with-num-1", key="key", signature_version="s3v4", is_secure=False, expected_url="http://bucket-with-num-1.s3.us-west-1.amazonaws.com/key", ) # Regions outside of the 'aws' partition. # These should still default to virtual hosted addressing # unless explicitly configured otherwise. yield dict( region="cn-north-1", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.cn-north-1.amazonaws.com.cn/key", ) # This isn't actually supported because cn-north-1 is sigv4 only, # but we'll still double check that our internal logic is correct # when building the expected url. yield dict( region="cn-north-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.cn-north-1.amazonaws.com.cn/key", ) # If the request is unsigned, we should have the default # fix_s3_host behavior which is to use virtual hosting where # possible but fall back to path style when needed. yield dict( region="cn-north-1", bucket="bucket", key="key", signature_version=UNSIGNED, expected_url="https://bucket.s3.cn-north-1.amazonaws.com.cn/key", ) yield dict( region="cn-north-1", bucket="bucket.dot", key="key", signature_version=UNSIGNED, expected_url="https://s3.cn-north-1.amazonaws.com.cn/bucket.dot/key", ) # And of course you can explicitly specify which style to use. virtual_hosting = {"addressing_style": "virtual"} yield dict( region="cn-north-1", bucket="bucket", key="key", signature_version=UNSIGNED, s3_config=virtual_hosting, expected_url="https://bucket.s3.cn-north-1.amazonaws.com.cn/key", ) path_style = {"addressing_style": "path"} yield dict( region="cn-north-1", bucket="bucket", key="key", signature_version=UNSIGNED, s3_config=path_style, expected_url="https://s3.cn-north-1.amazonaws.com.cn/bucket/key", ) # If you don't have a DNS compatible bucket, we use path style. yield dict( region="us-west-2", bucket="bucket.dot", key="key", expected_url="https://s3.us-west-2.amazonaws.com/bucket.dot/key", ) yield dict( region="us-east-1", bucket="bucket.dot", key="key", expected_url="https://s3.amazonaws.com/bucket.dot/key", ) yield dict( region="us-east-1", bucket="BucketName", key="key", expected_url="https://s3.amazonaws.com/BucketName/key", ) yield dict( region="us-west-1", bucket="bucket_name", key="key", expected_url="https://s3.us-west-1.amazonaws.com/bucket_name/key", ) yield dict( region="us-west-1", bucket="-bucket-name", key="key", expected_url="https://s3.us-west-1.amazonaws.com/-bucket-name/key", ) yield dict( region="us-west-1", bucket="bucket-name-", key="key", expected_url="https://s3.us-west-1.amazonaws.com/bucket-name-/key", ) yield dict( region="us-west-1", bucket="aa", key="key", expected_url="https://s3.us-west-1.amazonaws.com/aa/key", ) yield dict( region="us-west-1", bucket="a" * 64, key="key", expected_url=( "https://s3.us-west-1.amazonaws.com/%s/key" % ("a" * 64) ), ) # Custom endpoint url should always be used. yield dict( customer_provided_endpoint="https://my-custom-s3/", bucket="foo", key="bar", expected_url="https://my-custom-s3/foo/bar", ) yield dict( customer_provided_endpoint="https://my-custom-s3/", bucket="bucket.dots", key="bar", expected_url="https://my-custom-s3/bucket.dots/bar", ) # Doesn't matter what region you specify, a custom endpoint url always # wins. yield dict( customer_provided_endpoint="https://my-custom-s3/", region="us-west-2", bucket="foo", key="bar", expected_url="https://my-custom-s3/foo/bar", ) # Explicitly configuring "virtual" addressing_style. virtual_hosting = {"addressing_style": "virtual"} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=virtual_hosting, expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", s3_config=virtual_hosting, expected_url="https://bucket.s3.us-west-2.amazonaws.com/key", ) yield dict( region="eu-central-1", bucket="bucket", key="key", s3_config=virtual_hosting, expected_url="https://bucket.s3.eu-central-1.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=virtual_hosting, customer_provided_endpoint="https://foo.amazonaws.com", expected_url="https://bucket.foo.amazonaws.com/key", ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=virtual_hosting, expected_url="https://bucket.s3.unknown.amazonaws.com/key", ) # Test us-gov with virtual addressing. yield dict( region="us-gov-west-1", bucket="bucket", key="key", s3_config=virtual_hosting, expected_url="https://bucket.s3.us-gov-west-1.amazonaws.com/key", ) yield dict( region="us-gov-west-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.us-gov-west-1.amazonaws.com/key", ) yield dict( region="fips-us-gov-west-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3-fips.us-gov-west-1.amazonaws.com/key", ) # Test path style addressing. path_style = {"addressing_style": "path"} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=path_style, expected_url="https://s3.amazonaws.com/bucket/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=path_style, customer_provided_endpoint="https://foo.amazonaws.com/", expected_url="https://foo.amazonaws.com/bucket/key", ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=path_style, expected_url="https://s3.unknown.amazonaws.com/bucket/key", ) # S3 accelerate use_accelerate = {"use_accelerate_endpoint": True} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_accelerate, expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) yield dict( # region is ignored with S3 accelerate. region="us-west-2", bucket="bucket", key="key", s3_config=use_accelerate, expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) # Provided endpoints still get recognized as accelerate endpoints. yield dict( region="us-east-1", bucket="bucket", key="key", customer_provided_endpoint="https://s3-accelerate.amazonaws.com", expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", customer_provided_endpoint="http://s3-accelerate.amazonaws.com", expected_url="http://bucket.s3-accelerate.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_accelerate, is_secure=False, # Note we're using http:// because is_secure=False. expected_url="http://bucket.s3-accelerate.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", # s3-accelerate must be the first part of the url. customer_provided_endpoint="https://foo.s3-accelerate.amazonaws.com", expected_url="https://foo.s3-accelerate.amazonaws.com/bucket/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", # The endpoint must be an Amazon endpoint. customer_provided_endpoint="https://s3-accelerate.notamazon.com", expected_url="https://s3-accelerate.notamazon.com/bucket/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", # Extra components must be whitelisted. customer_provided_endpoint="https://s3-accelerate.foo.amazonaws.com", expected_url="https://s3-accelerate.foo.amazonaws.com/bucket/key", ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=use_accelerate, expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) # Use virtual even if path is specified for s3 accelerate because # path style will not work with S3 accelerate. yield dict( region="us-east-1", bucket="bucket", key="key", s3_config={ "use_accelerate_endpoint": True, "addressing_style": "path", }, expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) # S3 dual stack endpoints. use_dualstack = {"use_dualstack_endpoint": True} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_dualstack, signature_version="s3", # Still default to virtual hosted when possible on sigv2. expected_url="https://bucket.s3.dualstack.us-east-1.amazonaws.com/key", ) yield dict( region=None, bucket="bucket", key="key", s3_config=use_dualstack, # Uses us-east-1 for no region set. expected_url="https://bucket.s3.dualstack.us-east-1.amazonaws.com/key", ) yield dict( region="aws-global", bucket="bucket", key="key", s3_config=use_dualstack, # The aws-global pseudo region does not support dualstack and should # be resolved to us-east-1. expected_url=( "https://bucket.s3.dualstack.us-east-1.amazonaws.com/key" ), ) yield dict( region="us-west-2", bucket="bucket", key="key", s3_config=use_dualstack, signature_version="s3", # Still default to virtual hosted when possible on sigv2. expected_url="https://bucket.s3.dualstack.us-west-2.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_dualstack, signature_version="s3v4", expected_url="https://bucket.s3.dualstack.us-east-1.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", s3_config=use_dualstack, signature_version="s3v4", expected_url="https://bucket.s3.dualstack.us-west-2.amazonaws.com/key", ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=use_dualstack, signature_version="s3v4", expected_url="https://bucket.s3.dualstack.unknown.amazonaws.com/key", ) # Non DNS compatible buckets use path style for dual stack. yield dict( region="us-west-2", bucket="bucket.dot", key="key", s3_config=use_dualstack, # Still default to virtual hosted when possible. expected_url=( "https://s3.dualstack.us-west-2.amazonaws.com/bucket.dot/key" ), ) # Supports is_secure (use_ssl=False in create_client()). yield dict( region="us-west-2", bucket="bucket.dot", key="key", is_secure=False, s3_config=use_dualstack, # Still default to virtual hosted when possible. expected_url=( "http://s3.dualstack.us-west-2.amazonaws.com/bucket.dot/key" ), ) # Is path style is requested, we should use it, even if the bucket is # DNS compatible. force_path_style = { "use_dualstack_endpoint": True, "addressing_style": "path", } yield dict( region="us-west-2", bucket="bucket", key="key", s3_config=force_path_style, # Still default to virtual hosted when possible. expected_url="https://s3.dualstack.us-west-2.amazonaws.com/bucket/key", ) # Accelerate + dual stack use_accelerate_dualstack = { "use_accelerate_endpoint": True, "use_dualstack_endpoint": True, } yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_accelerate_dualstack, expected_url=( "https://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) yield dict( # Region is ignored with S3 accelerate. region="us-west-2", bucket="bucket", key="key", s3_config=use_accelerate_dualstack, expected_url=( "https://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) # Only s3-accelerate overrides a customer endpoint. yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_dualstack, customer_provided_endpoint="https://s3-accelerate.amazonaws.com", expected_url=("https://bucket.s3-accelerate.amazonaws.com/key"), ) yield dict( region="us-east-1", bucket="bucket", key="key", # Dualstack is whitelisted. customer_provided_endpoint=( "https://s3-accelerate.dualstack.amazonaws.com" ), expected_url=( "https://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) yield dict( region="us-east-1", bucket="bucket", key="key", # Even whitelisted parts cannot be duplicated. customer_provided_endpoint=( "https://s3-accelerate.dualstack.dualstack.amazonaws.com" ), expected_url=( "https://s3-accelerate.dualstack.dualstack" ".amazonaws.com/bucket/key" ), ) yield dict( region="us-east-1", bucket="bucket", key="key", # More than two extra parts is not allowed. customer_provided_endpoint=( "https://s3-accelerate.dualstack.dualstack.dualstack" ".amazonaws.com" ), expected_url=( "https://s3-accelerate.dualstack.dualstack.dualstack.amazonaws.com" "/bucket/key" ), ) yield dict( region="us-east-1", bucket="bucket", key="key", # Extra components must be whitelisted. customer_provided_endpoint="https://s3-accelerate.foo.amazonaws.com", expected_url="https://s3-accelerate.foo.amazonaws.com/bucket/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_accelerate_dualstack, is_secure=False, # Note we're using http:// because is_secure=False. expected_url=( "http://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) # Use virtual even if path is specified for s3 accelerate because # path style will not work with S3 accelerate. use_accelerate_dualstack["addressing_style"] = "path" yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=use_accelerate_dualstack, expected_url=( "https://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) # Access-point arn cases yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="myendpoint/key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/myendpoint/key" ), ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="foo/myendpoint/key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/foo/myendpoint/key" ), ) yield dict( # Note: The access-point arn has us-west-2 and the client's region is # us-east-1, for the defauldict the access-point arn region is used. region="us-east-1", bucket=accesspoint_arn, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="s3-external-1", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="aws-global", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="unknown", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="cn-north-1", bucket=accesspoint_arn_cn, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "cn-north-1.amazonaws.com.cn/key" ), ) yield dict( region="cn-northwest-1", bucket=accesspoint_arn_cn, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "cn-north-1.amazonaws.com.cn/key" ), ) yield dict( region="us-gov-west-1", bucket=accesspoint_arn_gov, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-gov-west-1.amazonaws.com/key" ), ) yield dict( region="fips-us-gov-west-1", bucket=accesspoint_arn_gov, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips." "us-gov-west-1.amazonaws.com/key" ), ) yield dict( region="fips-us-gov-west-1", bucket=accesspoint_arn_gov, key="key", s3_config={"use_arn_region": False}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips." "us-gov-west-1.amazonaws.com/key" ), ) yield dict( region="fips-us-gov-west-1", bucket=accesspoint_cross_region_arn_gov, s3_config={"use_arn_region": True}, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips." "us-gov-east-1.amazonaws.com/key" ), ) yield dict( region="us-gov-west-1", bucket=accesspoint_arn_gov, key="key", s3_config={"use_arn_region": False}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips." "us-gov-west-1.amazonaws.com/key" ), use_fips_endpoint=True, ) yield dict( region="us-gov-west-1", bucket=accesspoint_cross_region_arn_gov, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips." "us-gov-east-1.amazonaws.com/key" ), use_fips_endpoint=True, ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", is_secure=False, expected_url=( "http://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) # Dual-stack with access-point arn yield dict( # Note: The access-point arn has us-west-2 and the client's region is # us-east-1, for the defauldict the access-point arn region is used. region="us-east-1", bucket=accesspoint_arn, key="key", s3_config={ "use_dualstack_endpoint": True, }, expected_url=( "https://myendpoint-123456789012.s3-accesspoint.dualstack." "us-west-2.amazonaws.com/key" ), ) yield dict( region="us-gov-west-1", bucket=accesspoint_arn_gov, key="key", s3_config={ "use_dualstack_endpoint": True, }, expected_url=( "https://myendpoint-123456789012.s3-accesspoint.dualstack." "us-gov-west-1.amazonaws.com/key" ), ) yield dict( region="fips-us-gov-west-1", bucket=accesspoint_arn_gov, key="key", s3_config={ "use_arn_region": True, "use_dualstack_endpoint": True, }, expected_url=( "https://myendpoint-123456789012.s3-accesspoint-fips.dualstack." "us-gov-west-1.amazonaws.com/key" ), ) # None of the various s3 settings related to paths should affect what # endpoint to use when an access-point is provided. yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", s3_config={"adressing_style": "auto"}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", s3_config={"adressing_style": "virtual"}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", s3_config={"adressing_style": "path"}, expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) # Use us-east-1 regional endpoindicts: regional us_east_1_regional_endpoint = {"us_east_1_regional_endpoint": "regional"} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, expected_url=("https://bucket.s3.us-east-1.amazonaws.com/key"), ) yield dict( region="us-west-2", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, expected_url=("https://bucket.s3.us-west-2.amazonaws.com/key"), ) yield dict( region=None, bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, expected_url=("https://bucket.s3.amazonaws.com/key"), ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, expected_url=("https://bucket.s3.unknown.amazonaws.com/key"), ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config={ "us_east_1_regional_endpoint": "regional", "use_dualstack_endpoint": True, }, expected_url=( "https://bucket.s3.dualstack.us-east-1.amazonaws.com/key" ), ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config={ "us_east_1_regional_endpoint": "regional", "use_accelerate_endpoint": True, }, expected_url=("https://bucket.s3-accelerate.amazonaws.com/key"), ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config={ "us_east_1_regional_endpoint": "regional", "use_accelerate_endpoint": True, "use_dualstack_endpoint": True, }, expected_url=( "https://bucket.s3-accelerate.dualstack.amazonaws.com/key" ), ) # Use us-east-1 regional endpoindicts: legacy us_east_1_regional_endpoint_legacy = { "us_east_1_regional_endpoint": "legacy" } yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint_legacy, expected_url=("https://bucket.s3.amazonaws.com/key"), ) yield dict( region=None, bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint_legacy, expected_url=("https://bucket.s3.amazonaws.com/key"), ) yield dict( region="unknown", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint_legacy, expected_url=("https://bucket.s3.unknown.amazonaws.com/key"), ) s3_object_lambda_arn_gov = ( "arn:aws-us-gov:s3-object-lambda:us-gov-west-1:" "123456789012:accesspoint:mybanner" ) yield dict( region="fips-us-gov-west-1", bucket=s3_object_lambda_arn_gov, key="key", expected_url=( "https://mybanner-123456789012.s3-object-lambda-fips." "us-gov-west-1.amazonaws.com/key" ), ) yield dict( region="us-gov-west-1", bucket=s3_object_lambda_arn_gov, key="key", expected_url=( "https://mybanner-123456789012.s3-object-lambda-fips." "us-gov-west-1.amazonaws.com/key" ), use_fips_endpoint=True, ) s3_object_lambda_cross_region_arn_gov = ( "arn:aws-us-gov:s3-object-lambda:us-gov-east-1:" "123456789012:accesspoint:mybanner" ) yield dict( region="fips-us-gov-west-1", bucket=s3_object_lambda_cross_region_arn_gov, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://mybanner-123456789012.s3-object-lambda-fips." "us-gov-east-1.amazonaws.com/key" ), ) yield dict( region="us-gov-west-1", bucket=s3_object_lambda_cross_region_arn_gov, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://mybanner-123456789012.s3-object-lambda-fips." "us-gov-east-1.amazonaws.com/key" ), use_fips_endpoint=True, ) s3_object_lambda_arn = ( "arn:aws:s3-object-lambda:us-east-1:" "123456789012:accesspoint:mybanner" ) yield dict( region="aws-global", bucket=s3_object_lambda_arn, key="key", s3_config={"use_arn_region": True}, expected_url=( "https://mybanner-123456789012.s3-object-lambda." "us-east-1.amazonaws.com/key" ), ) def _s3_addressing_invalid_test_cases(): # client region does not match access point ARN region and use_arn_region # is False. If sent to service, this results in an "invalid access point" # response. We expect it to be caught by the S3 endpoints ruleset. yield dict( region="us-east-1", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": False}, expected_exception_type=UnsupportedS3AccesspointConfigurationError, expected_exception_regex=( "region from ARN `us-west-2` does not match client region " "`us-east-1`" ), ) yield dict( region="cn-northwest-1", bucket=accesspoint_arn_cn, key="key", s3_config={"use_arn_region": False}, expected_exception_type=UnsupportedS3AccesspointConfigurationError, expected_exception_regex=( "region from ARN `cn-north-1` does not match client region " "`cn-northwest-1`" ), ) yield dict( region="unknown", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": False}, expected_exception_type=UnsupportedS3AccesspointConfigurationError, expected_exception_regex=None, ) yield dict( region="us-east-1", bucket=accesspoint_arn, key="key", s3_config={"use_arn_region": False}, expected_exception_type=UnsupportedS3AccesspointConfigurationError, expected_exception_regex=None, ) @pytest.mark.parametrize("test_case", _s3_addressing_test_cases()) def test_correct_url_used_for_s3(test_case): # Test that given various sets of config options and bucket names, # we construct the expect endpoint url. _verify_expected_endpoint_url(**test_case) @pytest.mark.parametrize("test_case", _s3_addressing_invalid_test_cases()) def test_correct_exception_raise_for_s3(test_case): # Test that invalid sets of config options and bucket names, result in # appropriate exceptions. _verify_expected_exception(**test_case) def _verify_expected_endpoint_url( region=None, bucket="bucket", key="key", s3_config=None, is_secure=True, customer_provided_endpoint=None, expected_url=None, signature_version=None, use_fips_endpoint=None, ): s3 = _create_s3_client( region=region, is_secure=is_secure, endpoint_url=customer_provided_endpoint, s3_config=s3_config, signature_version=signature_version, use_fips_endpoint=use_fips_endpoint, ) with ClientHTTPStubber(s3) as http_stubber: http_stubber.add_response() s3.put_object(Bucket=bucket, Key=key, Body=b"bar") assert http_stubber.requests[0].url == expected_url def _verify_expected_exception( expected_exception_type, expected_exception_regex=None, region=None, bucket="bucket", key="key", s3_config=None, is_secure=True, customer_provided_endpoint=None, signature_version=None, use_fips_endpoint=None, ): s3 = _create_s3_client( region=region, is_secure=is_secure, endpoint_url=customer_provided_endpoint, s3_config=s3_config, signature_version=signature_version, use_fips_endpoint=use_fips_endpoint, ) with ClientHTTPStubber(s3) as http_stubber: http_stubber.add_response() with pytest.raises( expected_exception_type, match=expected_exception_regex ): s3.put_object(Bucket=bucket, Key=key, Body=b"bar") def _create_s3_client( region, is_secure, endpoint_url, s3_config, signature_version, use_fips_endpoint=None, ): environ = {} with mock.patch("os.environ", environ): environ["AWS_ACCESS_KEY_ID"] = "access_key" environ["AWS_SECRET_ACCESS_KEY"] = "secret_key" environ["AWS_CONFIG_FILE"] = "no-exist-foo" environ["AWS_SHARED_CREDENTIALS_FILE"] = "no-exist-foo" session = create_session() session.config_filename = "no-exist-foo" config = Config( signature_version=signature_version, s3=s3_config, use_fips_endpoint=use_fips_endpoint, ) s3 = session.create_client( "s3", region_name=region, use_ssl=is_secure, config=config, endpoint_url=endpoint_url, ) return s3 def _addressing_for_presigned_url_test_cases(): # us-east-1, or the "global" endpoint. A signature version of # None means the user doesn't have signature version configured. yield dict( region="us-east-1", bucket="bucket", key="key", signature_version=None, expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-1", bucket="bucket", key="key", signature_version="s3v4", s3_config={"addressing_style": "path"}, expected_url="https://s3.amazonaws.com/bucket/key", ) # A region that supports both 's3' and 's3v4'. yield dict( region="us-west-2", bucket="bucket", key="key", signature_version=None, expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3v4", s3_config={"addressing_style": "path"}, expected_url="https://s3.us-west-2.amazonaws.com/bucket/key", ) # An 's3v4' only region. yield dict( region="us-east-2", bucket="bucket", key="key", signature_version=None, expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-2", bucket="bucket", key="key", signature_version="s3", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-2", bucket="bucket", key="key", signature_version="s3v4", expected_url="https://bucket.s3.amazonaws.com/key", ) yield dict( region="us-east-2", bucket="bucket", key="key", signature_version="s3v4", s3_config={"addressing_style": "path"}, expected_url="https://s3.us-east-2.amazonaws.com/bucket/key", ) # Dualstack endpoints yield dict( region="us-west-2", bucket="bucket", key="key", signature_version=None, s3_config={"use_dualstack_endpoint": True}, expected_url="https://bucket.s3.dualstack.us-west-2.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3", s3_config={"use_dualstack_endpoint": True}, expected_url="https://bucket.s3.dualstack.us-west-2.amazonaws.com/key", ) yield dict( region="us-west-2", bucket="bucket", key="key", signature_version="s3v4", s3_config={"use_dualstack_endpoint": True}, expected_url="https://bucket.s3.dualstack.us-west-2.amazonaws.com/key", ) # Accelerate yield dict( region="us-west-2", bucket="bucket", key="key", signature_version=None, s3_config={"use_accelerate_endpoint": True}, expected_url="https://bucket.s3-accelerate.amazonaws.com/key", ) # A region that we don't know about. yield dict( region="boto-west-1", bucket="bucket", key="key", signature_version=None, expected_url="https://bucket.s3.amazonaws.com/key", ) # Customer provided URL results in us leaving the host untouched. yield dict( region="us-west-2", bucket="bucket", key="key", signature_version=None, customer_provided_endpoint="https://foo.com/", expected_url="https://foo.com/bucket/key", ) # Access-point yield dict( region="us-west-2", bucket=accesspoint_arn, key="key", expected_url=( "https://myendpoint-123456789012.s3-accesspoint." "us-west-2.amazonaws.com/key" ), ) # Use us-east-1 regional endpoint configuration cases us_east_1_regional_endpoint = {"us_east_1_regional_endpoint": "regional"} yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, signature_version="s3", expected_url=("https://bucket.s3.us-east-1.amazonaws.com/key"), ) yield dict( region="us-east-1", bucket="bucket", key="key", s3_config=us_east_1_regional_endpoint, signature_version="s3v4", expected_url=("https://bucket.s3.us-east-1.amazonaws.com/key"), ) # Bucket names that contain dots or are otherwise not virtual host style # compatible should always resolve to a regional endpoint. # https://github.com/boto/botocore/issues/2798 yield dict( region="us-west-1", bucket="foo.bar.biz", key="key", signature_version="s3", expected_url="https://s3.us-west-1.amazonaws.com/foo.bar.biz/key", ) @pytest.mark.parametrize( "test_case", _addressing_for_presigned_url_test_cases() ) def test_addressing_for_presigned_urls(test_case): # Here we're just focusing on the addressing mode used for presigned URLs. # We special case presigned URLs due to backward compatibility. _verify_presigned_url_addressing(**test_case) def _verify_presigned_url_addressing( region=None, bucket="bucket", key="key", s3_config=None, is_secure=True, customer_provided_endpoint=None, expected_url=None, signature_version=None, ): s3 = _create_s3_client( region=region, is_secure=is_secure, endpoint_url=customer_provided_endpoint, s3_config=s3_config, signature_version=signature_version, ) url = s3.generate_presigned_url( "get_object", {"Bucket": bucket, "Key": key} ) # We're not trying to verify the params for URL presigning, # those are tested elsewhere. We just care about the hostname/path. parts = urlsplit(url) actual = "%s://%s%s" % parts[:3] assert actual == expected_url class TestS3XMLPayloadEscape(BaseS3OperationTest): def assert_correct_content_md5(self, request): content_md5_bytes = get_md5(request.body).digest() content_md5 = base64.b64encode(content_md5_bytes) self.assertEqual(content_md5, request.headers["Content-MD5"]) def test_escape_keys_in_xml_delete_objects(self): self.http_stubber.add_response() with self.http_stubber: self.client.delete_objects( Bucket="mybucket", Delete={"Objects": [{"Key": "some\r\n\rkey"}]}, ) request = self.http_stubber.requests[0] self.assertNotIn(b"\r\n\r", request.body) self.assertIn(b" ", request.body) self.assert_correct_content_md5(request) def test_escape_keys_in_xml_put_bucket_lifecycle_configuration(self): self.http_stubber.add_response() with self.http_stubber: self.client.put_bucket_lifecycle_configuration( Bucket="mybucket", LifecycleConfiguration={ "Rules": [ { "Prefix": "my\r\n\rprefix", "Status": "ENABLED", } ] }, ) request = self.http_stubber.requests[0] self.assertNotIn(b"my\r\n\rprefix", request.body) self.assertIn(b"my prefix", request.body) self.assert_correct_content_md5(request)