diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-53be3ce.json b/.changes/next-release/bugfix-AWSSDKforJavav2-53be3ce.json new file mode 100644 index 000000000000..044b15412364 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-53be3ce.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Adds support for automatically transforming a FIPS pesudo region (e.g. \"fips-us-west-2\"), into a standard region name + setting the `fips_enabled` flag to `true`." +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 69d859445eb4..2a960f43a4eb 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -55,6 +55,8 @@ import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Pair; +import software.amazon.awssdk.utils.StringUtils; /** * An SDK-internal implementation of the methods in {@link AwsClientBuilder}, {@link AwsAsyncClientBuilder} and @@ -80,6 +82,9 @@ public abstract class AwsDefaultClientBuilder { private static final Logger log = Logger.loggerFor(AwsClientBuilder.class); private static final String DEFAULT_ENDPOINT_PROTOCOL = "https"; + private static final String[] FIPS_SEARCH = {"fips-", "-fips"}; + private static final String[] FIPS_REPLACE = {"", ""}; + private final AutoDefaultsModeDiscovery autoDefaultsModeDiscovery; protected AwsDefaultClientBuilder() { @@ -374,7 +379,17 @@ private RetryPolicy resolveAwsRetryPolicy(SdkClientConfiguration config) { @Override public final BuilderT region(Region region) { - clientConfiguration.option(AwsClientOption.AWS_REGION, region); + Region regionToSet = region; + Boolean fipsEnabled = null; + + if (region != null) { + Pair> transformedRegion = transformFipsPseudoRegionIfNecessary(region); + regionToSet = transformedRegion.left(); + fipsEnabled = transformedRegion.right().orElse(null); + } + + clientConfiguration.option(AwsClientOption.AWS_REGION, regionToSet); + clientConfiguration.option(AwsClientOption.FIPS_ENDPOINT_ENABLED, fipsEnabled); return thisBuilder(); } @@ -433,4 +448,20 @@ public final BuilderT defaultsMode(DefaultsMode defaultsMode) { public final void setDefaultsMode(DefaultsMode defaultsMode) { defaultsMode(defaultsMode); } + + /** + * If the region is a FIPS pseudo region (contains "fips"), this method returns a pair of values, the left side being the + * region with the "fips" string removed, and the right being {@code true}. Otherwise, the region is returned + * unchanged, and the right will be empty. + */ + private static Pair> transformFipsPseudoRegionIfNecessary(Region region) { + String id = region.id(); + String newId = StringUtils.replaceEach(id, FIPS_SEARCH, FIPS_REPLACE); + if (!newId.equals(id)) { + log.info(() -> String.format("Replacing input region %s with %s and setting fipsEnabled to true", id, newId)); + return Pair.of(Region.of(newId), Optional.of(true)); + } + + return Pair.of(region, Optional.empty()); + } } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java index 23f020c68c6c..413a8e8bcb0f 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java @@ -42,7 +42,7 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.signer.Aws4Signer; -import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; @@ -104,6 +104,17 @@ public void buildWithRegionShouldHaveCorrectEndpointAndSigningRegion() { assertThat(client.clientConfiguration.option(SERVICE_SIGNING_NAME)).isEqualTo(SIGNING_NAME); } + @Test + public void buildWithFipsRegionThenNonFipsFipsEnabledFlagUnset() { + TestClient client = testClientBuilder() + .region(Region.of("us-west-2-fips")) // first call to setter sets the flag + .region(Region.of("us-west-2"))// second call should clear + .build(); + + assertThat(client.clientConfiguration.option(AwsClientOption.AWS_REGION)).isEqualTo(Region.US_WEST_2); + assertThat(client.clientConfiguration.option(AwsClientOption.FIPS_ENDPOINT_ENABLED)).isNull(); + } + @Test public void buildWithEndpointShouldHaveCorrectEndpointAndSigningRegion() { TestClient client = testClientBuilder().region(Region.US_WEST_1).endpointOverride(ENDPOINT).build(); diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/FipsPseudoRegionTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/FipsPseudoRegionTest.java new file mode 100644 index 000000000000..c20f3d924157 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/FipsPseudoRegionTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.awscore.client.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION; +import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.SIGNER; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.AttributeMap; + +public class FipsPseudoRegionTest { + private static final AttributeMap MOCK_DEFAULTS = AttributeMap + .builder() + .put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(10)) + .build(); + + private static final String ENDPOINT_PREFIX = "s3"; + private static final String SIGNING_NAME = "demo"; + private static final String SERVICE_NAME = "Demo"; + + private SdkHttpClient.Builder defaultHttpClientBuilder; + + + @BeforeEach + public void setup() { + defaultHttpClientBuilder = mock(SdkHttpClient.Builder.class); + when(defaultHttpClientBuilder.buildWithDefaults(any())).thenReturn(mock(SdkHttpClient.class)); + } + + @ParameterizedTest + @MethodSource("testCases") + public void verifyRegion(TestCase tc) { + TestClient client = testClientBuilder().region(tc.inputRegion).build(); + + assertThat(client.clientConfiguration.option(AwsClientOption.AWS_REGION)).isEqualTo(tc.expectedClientRegion); + assertThat(client.clientConfiguration.option(AwsClientOption.FIPS_ENDPOINT_ENABLED)).isTrue(); + } + + public static List testCases() { + List testCases = new ArrayList<>(); + + testCases.add(new TestCase(Region.of("fips-us-west-2"), Region.of("us-west-2"))); + testCases.add(new TestCase(Region.of("us-west-2-fips"), Region.of("us-west-2"))); + testCases.add(new TestCase(Region.of("rekognition-fips.us-west-2"), Region.of("rekognition.us-west-2"))); + testCases.add(new TestCase(Region.of("rekognition.fips-us-west-2"), Region.of("rekognition.us-west-2"))); + testCases.add(new TestCase(Region.of("query-fips-us-west-2"), Region.of("query-us-west-2"))); + testCases.add(new TestCase(Region.of("fips-fips-us-west-2"), Region.of("us-west-2"))); + testCases.add(new TestCase(Region.of("fips-us-west-2-fips"), Region.of("us-west-2"))); + + + return testCases; + } + + private AwsClientBuilder testClientBuilder() { + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(SIGNER, mock(Signer.class)) + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + return new TestClientBuilder().credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig); + } + + private static class TestCase { + private Region inputRegion; + private Region expectedClientRegion; + + public TestCase(Region inputRegion, Region expectedClientRegion) { + this.inputRegion = inputRegion; + this.expectedClientRegion = expectedClientRegion; + } + } + + private static class TestClient { + private final SdkClientConfiguration clientConfiguration; + + public TestClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } + + private class TestClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + public TestClientBuilder() { + super(defaultHttpClientBuilder, null, null); + } + + @Override + protected TestClient buildClient() { + return new TestClient(super.syncClientConfiguration()); + } + + @Override + protected String serviceEndpointPrefix() { + return ENDPOINT_PREFIX; + } + + @Override + protected String signingName() { + return SIGNING_NAME; + } + + @Override + protected String serviceName() { + return SERVICE_NAME; + } + + @Override + protected AttributeMap serviceHttpConfig() { + return MOCK_DEFAULTS; + } + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3AccessPointEndpointResolver.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3AccessPointEndpointResolver.java index 3ac19aef65e3..68b90635a06d 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3AccessPointEndpointResolver.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3AccessPointEndpointResolver.java @@ -125,7 +125,7 @@ private void validateConfiguration(S3EndpointResolverContext context, S3AccessPo if (s3Resource.region().isPresent()) { validateRegion(s3Resource, serviceConfig, clientRegion, context.fipsEnabled()); } else { - validateGlobalConfiguration(serviceConfig, clientRegion); + validateGlobalConfiguration(context); } validatePartition(s3Resource, clientRegion); } @@ -170,7 +170,8 @@ private boolean clientRegionDiffersFromArnRegion(Region clientRegion, String arn return !removeFipsIfNeeded(clientRegion.id()).equals(arnRegion); } - private void validateGlobalConfiguration(S3Configuration serviceConfiguration, Region region) { + private void validateGlobalConfiguration(S3EndpointResolverContext context) { + S3Configuration serviceConfiguration = context.serviceConfiguration(); Validate.isTrue(serviceConfiguration.multiRegionEnabled(), "An Access Point ARN without a region value was passed as " + "a bucket parameter but multi-region is disabled. Check " + "client configuration, environment variables and system " @@ -178,7 +179,7 @@ private void validateGlobalConfiguration(S3Configuration serviceConfiguration, R Validate.isFalse(isDualstackEnabled(serviceConfiguration), S3_CONFIG_ERROR_MESSAGE, "dualstack, if the ARN contains no region."); - Validate.isFalse(isFipsRegion(region.toString()), S3_CONFIG_ERROR_MESSAGE, + Validate.isFalse(isFipsEnabled(context), S3_CONFIG_ERROR_MESSAGE, "a FIPS enabled region, if the ARN contains no region."); } @@ -275,7 +276,7 @@ private static Optional resolveSigningService(S3Resource resource) { return Optional.empty(); } - private boolean isFipsEnabled(S3EndpointResolverContext context) { + private static boolean isFipsEnabled(S3EndpointResolverContext context) { return context.fipsEnabled() || isFipsRegion(context.region().toString()); } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java index 18cae7db4682..9339ec4cf1f7 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java @@ -748,7 +748,7 @@ public void accessPointArn_NonFips_usGovEast1_region_FipsUsGovEast1_useArnRegion mockHttpClient.stubNextResponse(mockListObjectsResponse()); S3Client s3Client = clientBuilder().region(Region.of("fips-us-gov-east-1")) .serviceConfiguration(S3Configuration.builder().useArnRegionEnabled(false).build()).build(); - String accessPointArn = "arn:aws:s3:us-gov-east-1:123456789012:accesspoint:foobar"; + String accessPointArn = "arn:aws-us-gov:s3:us-gov-east-1:123456789012:accesspoint:foobar"; s3Client.getObject(GetObjectRequest.builder().bucket(accessPointArn).key("someKey").build()); assertThat(mockHttpClient.getLastRequest().getUri().getHost()) .isEqualTo("foobar-123456789012.s3-accesspoint-fips.us-gov-east-1.amazonaws.com"); @@ -759,7 +759,7 @@ public void accessPointArn_NonFips_usGovEast1_region_FipsUsGovEast1_useArnRegion mockHttpClient.stubNextResponse(mockListObjectsResponse()); S3Client s3Client = clientBuilder().region(Region.of("fips-us-gov-east-1")) .serviceConfiguration(S3Configuration.builder().useArnRegionEnabled(true).build()).build(); - String accessPointArn = "arn:aws:s3:us-gov-east-1:123456789012:accesspoint:foobar"; + String accessPointArn = "arn:aws-us-gov:s3:us-gov-east-1:123456789012:accesspoint:foobar"; s3Client.getObject(GetObjectRequest.builder().bucket(accessPointArn).key("someKey").build()); assertThat(mockHttpClient.getLastRequest().getUri().getHost()) .isEqualTo("foobar-123456789012.s3-accesspoint-fips.us-gov-east-1.amazonaws.com"); @@ -776,7 +776,7 @@ public void accessPointArn_usGovWest1_clientRegion_FipsUsGovEast1_useArnRegionTr .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The region field of the ARN being passed as a bucket parameter to an S3 operation does not " + "match the region the client was configured with. Cross region access not allowed for fips " - + "region in client or arn. Provided region: 'us-west-1'; client region:'fips-us-gov-east-1'."); + + "region in client or arn. Provided region: 'us-west-1'; client region:'us-gov-east-1'."); } @Test @@ -784,7 +784,7 @@ public void accessPointArn_NonFips_usGovWest1_region_FipsUsGovEast1_useArnRegion mockHttpClient.stubNextResponse(mockListObjectsResponse()); S3Client s3Client = clientBuilder().region(Region.of("fips-us-gov-east-1")) .serviceConfiguration(S3Configuration.builder().useArnRegionEnabled(true).dualstackEnabled(true).build()).build(); - String accessPointArn = "arn:aws:s3:us-gov-east-1:123456789012:accesspoint:foobar"; + String accessPointArn = "arn:aws-us-gov:s3:us-gov-east-1:123456789012:accesspoint:foobar"; s3Client.getObject(GetObjectRequest.builder().bucket(accessPointArn).key("someKey").build()); assertThat(mockHttpClient.getLastRequest().getUri().getHost()) .isEqualTo("foobar-123456789012.s3-accesspoint-fips.dualstack.us-gov-east-1.amazonaws.com");