# Copyright 2020 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 re from contextlib import contextmanager import pytest from tests import unittest, mock, BaseSessionTest, ClientHTTPStubber from botocore import exceptions from botocore.exceptions import ( UnsupportedS3ControlArnError, UnsupportedS3ControlConfigurationError, InvalidHostLabelError, ParamValidationError, ) from botocore.session import Session from botocore.compat import urlsplit from botocore.config import Config from botocore.awsrequest import AWSResponse ACCESSPOINT_ARN_TEST_CASES = [ # Outpost accesspoint arn test cases { 'arn': ( 'arn:aws:s3-outposts:us-west-2:123456789012:' 'outpost:op-01234567890123456:accesspoint:myaccesspoint' ), 'region': 'us-west-2', 'config': {}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-west-2.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': False}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-gov-east-1', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-gov-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-gov-east-1-fips', 'config': {'s3': {'use_arn_region': False}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, #{ # 'arn': 'arn:aws-us-gov:s3-outposts:fips-us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', # 'region': 'fips-us-gov-east-1', # 'config': {'s3': {'use_arn_region': True}}, # 'assertions': { # 'exception': 'UnsupportedS3ArnError', # } #}, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'region': 'us-gov-east-1-fips', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-gov-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'config': {'s3': {'use_dualstack_endpoint': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint', 'config': {'s3': {'use_accelerate_endpoint': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:myaccesspoint', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, ] BUCKET_ARN_TEST_CASES = [ # Outpost bucket arn test cases { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-west-2', 'config': {}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-west-2.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': False}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-west-2', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-gov-east-1', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-gov-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-gov-east-1-fips', 'config': {'s3': {'use_arn_region': False}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-us-gov:s3-outposts:fips-us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'fips-us-gov-east-1', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-gov-east-1-fips', 'config': {'s3': {'use_arn_region': True}}, 'assertions': { 'signing_name': 's3-outposts', 'netloc': 's3-outposts.us-gov-east-1.amazonaws.com', 'headers': { 'x-amz-outpost-id': 'op-01234567890123456', 'x-amz-account-id': '123456789012', }, } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket', 'region': 'us-west-2', 'config': {'s3': {'use_dualstack_endpoint': True}}, 'assertions': { 'exception': 'UnsupportedS3ControlConfigurationError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:bucket', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, { 'arn': 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket', 'assertions': { 'exception': 'UnsupportedS3ControlArnError', } }, ] V4_AUTH_REGEX = re.compile( r'AWS4-HMAC-SHA256 Credential=\w+/\d+/' r'(?P[a-z0-9-]+)/(?P[a-z0-9-]+)/' ) def _assert_signing_name(stubber, expected_name): request = stubber.requests[0] auth_header = request.headers['Authorization'].decode('utf-8') actual_name = V4_AUTH_REGEX.match(auth_header).group('name') assert expected_name == actual_name def _assert_netloc(stubber, expected_netloc): request = stubber.requests[0] url_parts = urlsplit(request.url) assert expected_netloc == url_parts.netloc def _assert_header(stubber, key, value): request = stubber.requests[0] assert key in request.headers actual_value = request.headers[key] if isinstance(actual_value, bytes): actual_value = actual_value.decode('utf-8') assert value == actual_value def _assert_headers(stubber, headers): for key, value in headers.items(): _assert_header(stubber, key, value) def _bootstrap_session(): session = Session() session.set_credentials('access_key', 'secret_key') return session def _bootstrap_client(session, region, **kwargs): client = session.create_client('s3control', region, **kwargs) stubber = ClientHTTPStubber(client) return client, stubber def _bootstrap_test_case_client(session, test_case): region = test_case.get('region', 'us-west-2') config = test_case.get('config', {}) config = Config(**config) return _bootstrap_client(session, region, config=config) @pytest.mark.parametrize("test_case", ACCESSPOINT_ARN_TEST_CASES) def test_accesspoint_arn_redirection(test_case): session = _bootstrap_session() client, stubber = _bootstrap_test_case_client(session, test_case) with _assert_test_case(test_case, client, stubber): client.get_access_point_policy(Name=test_case['arn']) @pytest.mark.parametrize("test_case", BUCKET_ARN_TEST_CASES) def test_bucket_arn_redirection(test_case): session = _bootstrap_session() client, stubber = _bootstrap_test_case_client(session, test_case) with _assert_test_case(test_case, client, stubber): client.get_bucket(Bucket=test_case['arn']) @contextmanager def _assert_test_case(test_case, client, stubber): stubber.add_response() assertions = test_case['assertions'] exception_raised = None try: with stubber: yield except Exception as e: if 'exception' not in assertions: raise exception_raised = e if 'exception' in assertions: exception_cls = getattr(exceptions, assertions['exception']) if exception_raised is None: raise RuntimeError( 'Expected exception "%s" was not raised' % exception_cls ) error_msg = ( 'Expected exception "%s", got "%s"' ) % (exception_cls, type(exception_raised)) assert isinstance(exception_raised, exception_cls), error_msg else: assert len(stubber.requests) == 1 if 'signing_name' in assertions: _assert_signing_name(stubber, assertions['signing_name']) if 'headers' in assertions: _assert_headers(stubber, assertions['headers']) if 'netloc' in assertions: _assert_netloc(stubber, assertions['netloc']) class TestS3ControlRedirection(unittest.TestCase): def setUp(self): self.session = _bootstrap_session() self.region = 'us-west-2' self._bootstrap_client() def _bootstrap_client(self, **kwargs): client, stubber = _bootstrap_client( self.session, self.region, **kwargs ) self.client = client self.stubber = stubber def test_outpost_id_redirection_dualstack(self): config = Config(s3={'use_dualstack_endpoint': True}) self._bootstrap_client(config=config) with self.assertRaises(UnsupportedS3ControlConfigurationError): self.client.create_bucket(Bucket='foo', OutpostId='op-123') def test_outpost_id_redirection_create_bucket(self): self.stubber.add_response() with self.stubber: self.client.create_bucket(Bucket='foo', OutpostId='op-123') _assert_netloc(self.stubber, 's3-outposts.us-west-2.amazonaws.com') _assert_header(self.stubber, 'x-amz-outpost-id', 'op-123') def test_outpost_id_redirection_list_regional_buckets(self): self.stubber.add_response() with self.stubber: self.client.list_regional_buckets( OutpostId='op-123', AccountId='1234', ) _assert_netloc(self.stubber, 's3-outposts.us-west-2.amazonaws.com') _assert_header(self.stubber, 'x-amz-outpost-id', 'op-123') def test_outpost_redirection_custom_endpoint(self): self._bootstrap_client(endpoint_url='https://outpost.foo.com/') self.stubber.add_response() with self.stubber: self.client.create_bucket(Bucket='foo', OutpostId='op-123') _assert_netloc(self.stubber, 'outpost.foo.com') _assert_header(self.stubber, 'x-amz-outpost-id', 'op-123') def test_normal_ap_request_has_correct_endpoint(self): self.stubber.add_response() with self.stubber: self.client.get_access_point_policy(Name='MyAp', AccountId='1234') _assert_netloc(self.stubber, '1234.s3-control.us-west-2.amazonaws.com') def test_normal_ap_request_custom_endpoint(self): self._bootstrap_client(endpoint_url='https://example.com/') self.stubber.add_response() with self.stubber: self.client.get_access_point_policy(Name='MyAp', AccountId='1234') _assert_netloc(self.stubber, '1234.example.com') def test_normal_bucket_request_has_correct_endpoint(self): self.stubber.add_response() with self.stubber: self.client.create_bucket(Bucket='foo') _assert_netloc(self.stubber, 's3-control.us-west-2.amazonaws.com') def test_arn_account_id_mismatch(self): arn = ( 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:' 'op-01234567890123456:accesspoint:myaccesspoint' ) with self.assertRaises(UnsupportedS3ControlArnError): self.client.get_access_point_policy(Name=arn, AccountId='1234') def test_create_access_point_does_not_expand_name(self): arn = ( 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:' 'op-01234567890123456:accesspoint:myaccesspoint' ) # AccountId will not be backfilled by the arn expansion and thus # fail parameter validation with self.assertRaises(ParamValidationError): self.client.create_access_point(Name=arn, Bucket='foo') def test_arn_invalid_host_label(self): config = Config(s3={'use_arn_region': True}) self._bootstrap_client(config=config) arn = ( 'arn:aws:s3-outposts:us-we$t-2:123456789012:outpost:' 'op-01234567890123456:accesspoint:myaccesspoint' ) with self.assertRaises(InvalidHostLabelError): self.client.get_access_point_policy(Name=arn) def test_unknown_arn_format(self): arn = 'arn:aws:foo:us-west-2:123456789012:bar:myresource' with self.assertRaises(UnsupportedS3ControlArnError): self.client.get_access_point_policy(Name=arn)