diff --git a/.changes/next-release/feature-AmazonS3-1112ce8.json b/.changes/next-release/feature-AmazonS3-1112ce8.json new file mode 100644 index 000000000000..be327b4446c3 --- /dev/null +++ b/.changes/next-release/feature-AmazonS3-1112ce8.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon S3", + "contributor": "", + "description": "Amazon S3 now supports AWS PrivateLink, providing direct access to S3 via a private endpoint within your virtual private network." +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java index 0147a7618e2b..5b17daee5b06 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java @@ -63,6 +63,8 @@ static ExecutionContext ClientExecutionParams executionParams, SdkClientConfiguration clientConfig, ExecutionAttributes executionAttributes) { + // Note: This is currently copied to DefaultS3Presigner and other presigners. Don't edit this without considering those + // as well. TODO: Probably don't copy all of this manually. SdkRequest originalRequest = executionParams.getInput(); AwsCredentialsProvider clientCredentials = clientConfig.option(AwsClientOption.CREDENTIALS_PROVIDER); @@ -92,8 +94,8 @@ static ExecutionContext .putAttribute(SdkExecutionAttribute.CLIENT_TYPE, clientConfig.option(SdkClientOption.CLIENT_TYPE)) .putAttribute(SdkExecutionAttribute.SERVICE_NAME, clientConfig.option(SdkClientOption.SERVICE_NAME)) .putAttribute(SdkExecutionAttribute.OPERATION_NAME, executionParams.getOperationName()) - .putAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN, - clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN)); + .putAttribute(SdkExecutionAttribute.CLIENT_ENDPOINT, clientConfig.option(SdkClientOption.ENDPOINT)) + .putAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN, clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN)); ExecutionInterceptorChain executionInterceptorChain = new ExecutionInterceptorChain(clientConfig.option(SdkClientOption.EXECUTION_INTERCEPTORS)); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java index 6aa85ae6c70f..1c51032b76e9 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.core.interceptor; +import java.net.URI; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.ServiceConfiguration; @@ -58,7 +59,13 @@ public class SdkExecutionAttribute { * If true indicates that the configured endpoint of the client is a value that was supplied as an override and not * generated from regional metadata. */ - public static final ExecutionAttribute ENDPOINT_OVERRIDDEN = new ExecutionAttribute<>("EndpointOverride"); + public static final ExecutionAttribute ENDPOINT_OVERRIDDEN = new ExecutionAttribute<>("EndpointOverridden"); + + /** + * The endpoint resolved at client creation time. This is either the endpointOverride (if {@link #ENDPOINT_OVERRIDDEN} is + * true) or the endpoint derived from the region metadata (if {@link #ENDPOINT_OVERRIDDEN} is false). + */ + public static final ExecutionAttribute CLIENT_ENDPOINT = new ExecutionAttribute<>("EndpointOverride"); protected SdkExecutionAttribute() { } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index e2b04d9da236..0165e799f39c 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -148,7 +148,6 @@ public URL getUrl(Consumer getUrlRequest) { public URL getUrl(GetUrlRequest getUrlRequest) { Region resolvedRegion = resolveRegionForGetUrl(getUrlRequest); URI resolvedEndpoint = resolveEndpoint(getUrlRequest.endpoint(), resolvedRegion); - boolean endpointOverridden = getUrlRequest.endpoint() != null; SdkHttpFullRequest marshalledRequest = createMarshalledRequest(getUrlRequest, resolvedEndpoint); @@ -162,7 +161,7 @@ public URL getUrl(GetUrlRequest getUrlRequest) { .request(marshalledRequest) .originalRequest(getObjectRequest) .region(resolvedRegion) - .endpointOverridden(endpointOverridden) + .endpointOverride(getUrlRequest.endpoint()) .serviceConfiguration(s3Configuration) .build(); 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 fd6edb22ecde..11060205e964 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 @@ -74,12 +74,13 @@ public ConfiguredS3SdkHttpRequest applyEndpointConfiguration(S3EndpointResolverC + "appear to be a valid S3 access point ARN."); URI accessPointUri = getUriForAccessPointResource(context, arnRegion, clientPartitionMetadata, s3EndpointResource); - String key = context.originalRequest().getValueForField("Key", String.class).orElse(null); + String path = buildPath(accessPointUri, context); + SdkHttpRequest httpRequest = context.request().toBuilder() .protocol(accessPointUri.getScheme()) .host(accessPointUri.getHost()) .port(accessPointUri.getPort()) - .encodedPath(key) + .encodedPath(path) .build(); String signingServiceModification = s3EndpointResource.parentS3Resource() @@ -94,6 +95,23 @@ public ConfiguredS3SdkHttpRequest applyEndpointConfiguration(S3EndpointResolverC .build(); } + private String buildPath(URI accessPointUri, S3EndpointResolverContext context) { + String key = context.originalRequest().getValueForField("Key", String.class).orElse(null); + + StringBuilder pathBuilder = new StringBuilder(); + if (accessPointUri.getPath() != null) { + pathBuilder.append(accessPointUri.getPath()); + } + + if (key != null) { + if (pathBuilder.length() > 0) { + pathBuilder.append('/'); + } + pathBuilder.append(key); + } + return pathBuilder.length() > 0 ? pathBuilder.toString() : null; + } + private String validateConfiguration(S3EndpointResolverContext context, S3Resource s3Resource) { Region region = context.region(); String arnRegion = s3Resource.region().orElseThrow(() -> new IllegalArgumentException( @@ -113,12 +131,6 @@ private String validateConfiguration(S3EndpointResolverContext context, S3Resour + "addressing enabled."); } - if (context.endpointOverridden()) { - throw new IllegalArgumentException("An access point ARN cannot be passed as a bucket parameter to an S3" - + " operation if the S3 client has been configured with an endpoint " - + "override."); - } - if (!isArnRegionEnabled(serviceConfiguration) && clientRegionDiffersFromArnRegion(region, arnRegion)) { throw new IllegalArgumentException( String.format("The region field of the ARN being passed as a bucket parameter to an S3 operation " @@ -157,11 +169,6 @@ private String getBucketName(S3EndpointResolverContext context) { private URI getUriForAccessPointResource(S3EndpointResolverContext context, String arnRegion, PartitionMetadata clientPartitionMetadata, S3AccessPointResource s3EndpointResource) { - - boolean dualstackEnabled = isDualstackEnabled(context.serviceConfiguration()); - boolean fipsRegionProvided = isFipsRegionProvided(context.region().toString(), arnRegion, - isArnRegionEnabled(context.serviceConfiguration())); - String accountId = s3EndpointResource.accountId().orElseThrow(() -> new IllegalArgumentException( "An S3 access point ARN must have an account ID")); String accessPointName = s3EndpointResource.accessPointName(); @@ -170,7 +177,11 @@ private URI getUriForAccessPointResource(S3EndpointResolverContext context, Stri return getOutpostAccessPointUri(context, arnRegion, clientPartitionMetadata, s3EndpointResource); } + boolean dualstackEnabled = isDualstackEnabled(context.serviceConfiguration()); + boolean fipsRegionProvided = isFipsRegionProvided(context.region().toString(), arnRegion, + isArnRegionEnabled(context.serviceConfiguration())); return S3AccessPointBuilder.create() + .endpointOverride(context.endpointOverride()) .accessPointName(accessPointName) .accountId(accountId) .fipsEnabled(fipsRegionProvided) @@ -200,6 +211,7 @@ private URI getOutpostAccessPointUri(S3EndpointResolverContext context, String a S3OutpostResource parentResource = (S3OutpostResource) s3EndpointResource.parentS3Resource().get(); return S3OutpostAccessPointBuilder.create() + .endpointOverride(context.endpointOverride()) .accountId(s3EndpointResource.accountId().get()) .outpostId(parentResource.outpostId()) .region(arnRegion) diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContext.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContext.java index 48cc41a9f37c..fcf26f02d3a7 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContext.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContext.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.s3.internal.endpoints; +import java.net.URI; import java.util.Objects; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.SdkRequest; @@ -31,14 +32,14 @@ public final class S3EndpointResolverContext { private final SdkRequest originalRequest; private final Region region; private final S3Configuration serviceConfiguration; - private final boolean endpointOverridden; + private final URI endpointOverride; private S3EndpointResolverContext(Builder builder) { this.request = builder.request; this.originalRequest = builder.originalRequest; this.region = builder.region; this.serviceConfiguration = builder.serviceConfiguration; - this.endpointOverridden = builder.endpointOverridden; + this.endpointOverride = builder.endpointOverride; } public static Builder builder() { @@ -61,8 +62,8 @@ public S3Configuration serviceConfiguration() { return serviceConfiguration; } - public boolean endpointOverridden() { - return endpointOverridden; + public URI endpointOverride() { + return endpointOverride; } @@ -75,7 +76,7 @@ public boolean equals(Object o) { return false; } S3EndpointResolverContext that = (S3EndpointResolverContext) o; - return endpointOverridden == that.endpointOverridden && + return Objects.equals(endpointOverride, that.endpointOverride) && Objects.equals(request, that.request) && Objects.equals(originalRequest, that.originalRequest) && Objects.equals(region, that.region) && @@ -89,12 +90,12 @@ public int hashCode() { hashCode = 31 * hashCode + Objects.hashCode(originalRequest()); hashCode = 31 * hashCode + Objects.hashCode(region()); hashCode = 31 * hashCode + Objects.hashCode(serviceConfiguration()); - hashCode = 31 * hashCode + Objects.hashCode(endpointOverridden()); + hashCode = 31 * hashCode + Objects.hashCode(endpointOverride()); return hashCode; } public Builder toBuilder() { - return builder().endpointOverridden(endpointOverridden) + return builder().endpointOverride(endpointOverride) .request(request) .originalRequest(originalRequest) .region(region) @@ -106,7 +107,7 @@ public static final class Builder { private SdkRequest originalRequest; private Region region; private S3Configuration serviceConfiguration; - private boolean endpointOverridden; + private URI endpointOverride; private Builder() { } @@ -131,8 +132,8 @@ public Builder serviceConfiguration(S3Configuration serviceConfiguration) { return this; } - public Builder endpointOverridden(boolean endpointOverridden) { - this.endpointOverridden = endpointOverridden; + public Builder endpointOverride(URI endpointOverride) { + this.endpointOverride = endpointOverride; return this; } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EndpointAddressInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EndpointAddressInterceptor.java index 3f6c4c9eece4..6fad5f0dfc33 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EndpointAddressInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EndpointAddressInterceptor.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.s3.internal.handlers; +import java.net.URI; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; import software.amazon.awssdk.awscore.AwsExecutionAttribute; @@ -35,8 +36,11 @@ public final class EndpointAddressInterceptor implements ExecutionInterceptor { public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { - boolean endpointOverride = + boolean endpointOverridden = Boolean.TRUE.equals(executionAttributes.getAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN)); + URI endpointOverride = endpointOverridden ? executionAttributes.getAttribute(SdkExecutionAttribute.CLIENT_ENDPOINT) + : null; + S3Configuration serviceConfiguration = (S3Configuration) executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_CONFIG); S3EndpointResolverContext resolverContext = @@ -44,7 +48,7 @@ public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, .request(context.httpRequest()) .originalRequest(context.request()) .region(executionAttributes.getAttribute(AwsExecutionAttribute.AWS_REGION)) - .endpointOverridden(endpointOverride) + .endpointOverride(endpointOverride) .serviceConfiguration(serviceConfiguration) .build(); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presigner/DefaultS3Presigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presigner/DefaultS3Presigner.java index 2180f1922f04..6fec094b9cd8 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presigner/DefaultS3Presigner.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presigner/DefaultS3Presigner.java @@ -115,6 +115,7 @@ public final class DefaultS3Presigner extends DefaultSdkPresigner implements S3P private final UploadPartRequestMarshaller uploadPartRequestMarshaller; private final CompleteMultipartUploadRequestMarshaller completeMultipartUploadRequestMarshaller; private final AbortMultipartUploadRequestMarshaller abortMultipartUploadRequestMarshaller; + private final SdkClientConfiguration clientConfiguration; private DefaultS3Presigner(Builder b) { super(b); @@ -128,9 +129,11 @@ private DefaultS3Presigner(Builder b) { this.clientInterceptors = initializeInterceptors(); + this.clientConfiguration = createClientConfiguration(); + // Copied from DefaultS3Client#init AwsS3ProtocolFactory protocolFactory = AwsS3ProtocolFactory.builder() - .clientConfiguration(createClientConfiguration()) + .clientConfiguration(clientConfiguration) .build(); // Copied from DefaultS3Client#getObject @@ -308,6 +311,9 @@ private ExecutionContext createExecutionContext(PresignRequest presignRequest, S .putAttribute(SdkExecutionAttribute.SERVICE_NAME, SERVICE_NAME) .putAttribute(SdkExecutionAttribute.OPERATION_NAME, operationName) .putAttribute(AwsSignerExecutionAttribute.SERVICE_CONFIG, serviceConfiguration()) + .putAttribute(SdkExecutionAttribute.CLIENT_ENDPOINT, clientConfiguration.option(SdkClientOption.ENDPOINT)) + .putAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN, + clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN)) .putAttribute(PRESIGNER_EXPIRATION, signatureExpiration); ExecutionInterceptorChain executionInterceptorChain = new ExecutionInterceptorChain(clientInterceptors); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3AccessPointBuilder.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3AccessPointBuilder.java index a2dbb2e5687c..23d3313edd33 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3AccessPointBuilder.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3AccessPointBuilder.java @@ -22,6 +22,7 @@ import java.util.regex.Pattern; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.utils.Validate; /** * This class is used to construct an endpoint host for an S3 access point. @@ -31,6 +32,7 @@ public class S3AccessPointBuilder { private static final Pattern HOSTNAME_COMPLIANT_PATTERN = Pattern.compile("[A-Za-z0-9\\-]+"); private static final int HOSTNAME_MAX_LENGTH = 63; + private URI endpointOverride; private Boolean dualstackEnabled; private String accessPointName; private String region; @@ -46,6 +48,14 @@ public static S3AccessPointBuilder create() { return new S3AccessPointBuilder(); } + /** + * The endpoint override configured on the client (null if no endpoint override was set). + */ + public S3AccessPointBuilder endpointOverride(URI endpointOverride) { + this.endpointOverride = endpointOverride; + return this; + } + /** * Enable DualStack endpoint. */ @@ -109,16 +119,36 @@ public URI toUri() { validateHostnameCompliant(accountId, "accountId"); validateHostnameCompliant(accessPointName, "accessPointName"); - String fipsSegment = Boolean.TRUE.equals(fipsEnabled) ? "fips-" : ""; + String uri; + if (endpointOverride == null) { + String fipsSegment = Boolean.TRUE.equals(fipsEnabled) ? "fips-" : ""; + String dualStackSegment = Boolean.TRUE.equals(dualstackEnabled) ? ".dualstack" : ""; + + uri = String.format("%s://%s-%s.s3-accesspoint%s.%s%s.%s", protocol, urlEncode(accessPointName), + accountId, dualStackSegment, fipsSegment, region, domain); + } else { + Validate.isTrue(!Boolean.TRUE.equals(fipsEnabled), + "FIPS regions are not supported with an endpoint override specified"); + Validate.isTrue(!Boolean.TRUE.equals(dualstackEnabled), + "Dual stack is not supported with an endpoint override specified"); + + StringBuilder uriSuffix = new StringBuilder(endpointOverride.getHost()); + if (endpointOverride.getPort() > 0) { + uriSuffix.append(":").append(endpointOverride.getPort()); + } + if (endpointOverride.getPath() != null) { + uriSuffix.append(endpointOverride.getPath()); + } + + uri = String.format("%s://%s-%s.%s", protocol, urlEncode(accessPointName), accountId, uriSuffix); + } - String dualStackSegment = Boolean.TRUE.equals(dualstackEnabled) ? ".dualstack" : ""; - String uriString = String.format("%s://%s-%s.s3-accesspoint%s.%s%s.%s", protocol, urlEncode(accessPointName), accountId, - dualStackSegment, fipsSegment, region, domain); - URI uri = URI.create(uriString); - if (uri.getHost() == null) { - throw SdkClientException.create("ARN region (" + region + ") resulted in an invalid URI:" + uri); + URI result = URI.create(uri); + if (result.getHost() == null) { + throw SdkClientException.create("Request resulted in an invalid URI: " + result); } - return uri; + + return result; } private static void validateHostnameCompliant(String hostnameComponent, String paramName) { diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3OutpostAccessPointBuilder.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3OutpostAccessPointBuilder.java index 3dec92e039f7..446eed710688 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3OutpostAccessPointBuilder.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3OutpostAccessPointBuilder.java @@ -16,9 +16,11 @@ package software.amazon.awssdk.services.s3.internal.resource; import static software.amazon.awssdk.utils.HostnameValidator.validateHostnameCompliant; +import static software.amazon.awssdk.utils.http.SdkHttpUtils.urlEncode; import java.net.URI; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkClientException; /** * This class is used to construct an endpoint for an S3 outpost access point. @@ -26,6 +28,7 @@ @SdkInternalApi public final class S3OutpostAccessPointBuilder { + private URI endpointOverride; private String accessPointName; private String outpostId; private String region; @@ -43,6 +46,14 @@ public static S3OutpostAccessPointBuilder create() { return new S3OutpostAccessPointBuilder(); } + /** + * The endpoint override configured on the client (null if no endpoint override was set). + */ + public S3OutpostAccessPointBuilder endpointOverride(URI endpointOverride) { + this.endpointOverride = endpointOverride; + return this; + } + public S3OutpostAccessPointBuilder accessPointName(String accessPointName) { this.accessPointName = accessPointName; return this; @@ -81,8 +92,27 @@ public URI toUri() { validateHostnameCompliant(accountId, "accountId", "outpost ARN"); validateHostnameCompliant(accessPointName, "accessPointName", "outpost ARN"); - String uriString = String.format("%s://%s-%s.%s.s3-outposts.%s.%s", protocol, accessPointName, accountId, outpostId, - region, domain); - return URI.create(uriString); + String uri; + if (endpointOverride == null) { + uri = String.format("%s://%s-%s.%s.s3-outposts.%s.%s", protocol, accessPointName, accountId, outpostId, + region, domain); + } else { + StringBuilder uriSuffix = new StringBuilder(endpointOverride.getHost()); + if (endpointOverride.getPort() > 0) { + uriSuffix.append(":").append(endpointOverride.getPort()); + } + if (endpointOverride.getPath() != null) { + uriSuffix.append(endpointOverride.getPath()); + } + + uri = String.format("%s://%s-%s.%s.%s", protocol, urlEncode(accessPointName), accountId, outpostId, uriSuffix); + } + + URI result = URI.create(uri); + if (result.getHost() == null) { + throw SdkClientException.create("Request resulted in an invalid URI: " + result); + } + + return result; } } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3Resource.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3Resource.java index 91f8ae93487c..0b93b2021691 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3Resource.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/resource/S3Resource.java @@ -16,12 +16,12 @@ package software.amazon.awssdk.services.s3.internal.resource; import java.util.Optional; -import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkProtectedApi; /** * A representation of an AWS S3 resource. See {@link S3ResourceType} for a list and description of all valid types. */ -@SdkInternalApi +@SdkProtectedApi public interface S3Resource extends AwsResource { /** * Gets the type of S3 resource represented by this object (e.g.: 'bucket_name'). See {@link S3ResourceType} for diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/EndpointOverrideEndpointResolutionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/EndpointOverrideEndpointResolutionTest.java new file mode 100644 index 000000000000..e800cf4643bb --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/EndpointOverrideEndpointResolutionTest.java @@ -0,0 +1,384 @@ +/* + * 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.services.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.signer.Presigner; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; + +@RunWith(Parameterized.class) +public class EndpointOverrideEndpointResolutionTest { + private final TestCase testCase; + private final SignerAndPresigner mockSigner; + private final MockSyncHttpClient mockHttpClient; + private final S3Client s3Client; + private final S3Presigner s3Presigner; + private final GetObjectRequest getObjectRequest; + private final S3Utilities s3Utilities; + + public interface SignerAndPresigner extends Signer, Presigner {} + + public EndpointOverrideEndpointResolutionTest(TestCase testCase) throws UnsupportedEncodingException { + this.testCase = testCase; + this.mockSigner = Mockito.mock(SignerAndPresigner.class); + this.mockHttpClient = new MockSyncHttpClient(); + this.s3Client = S3Client.builder() + .region(testCase.clientRegion) + .endpointOverride(testCase.endpointUrl) + .serviceConfiguration(testCase.s3Configuration) + .httpClient(mockHttpClient) + .overrideConfiguration(c -> c.putAdvancedOption(SdkAdvancedClientOption.SIGNER, mockSigner)) + .build(); + this.s3Presigner = S3Presigner.builder() + .region(testCase.clientRegion) + .endpointOverride(testCase.endpointUrl) + .serviceConfiguration(testCase.s3Configuration) + .build(); + this.s3Utilities = S3Utilities.builder() + .region(testCase.clientRegion) + .s3Configuration(testCase.s3Configuration) + .build(); + + this.getObjectRequest = testCase.getObjectBucketName == null + ? null + : GetObjectRequest.builder() + .bucket(testCase.getObjectBucketName) + .key("object") + .overrideConfiguration(c -> c.signer(mockSigner)) + .build(); + + mockHttpClient.stubNextResponse(S3MockUtils.mockListObjectsResponse()); + Mockito.when(mockSigner.sign(any(), any())).thenAnswer(r -> r.getArgumentAt(0, SdkHttpFullRequest.class)); + Mockito.when(mockSigner.presign(any(), any())).thenAnswer(r -> r.getArgumentAt(0, SdkHttpFullRequest.class) + .copy(h -> h.putRawQueryParameter("X-Amz-SignedHeaders", "host"))); + } + + @Test + public void s3Client_endpointSigningRegionAndServiceNamesAreCorrect() { + try { + if (getObjectRequest != null) { + s3Client.getObject(getObjectRequest); + } else { + s3Client.listBuckets(); + } + + assertThat(mockHttpClient.getLastRequest().getUri()).isEqualTo(testCase.expectedEndpoint); + assertThat(signingRegion()).isEqualTo(testCase.expectedSigningRegion); + assertThat(signingServiceName()).isEqualTo(testCase.expectedSigningServiceName); + assertThat(testCase.expectedException).isNull(); + } catch (RuntimeException e) { + if (testCase.expectedException == null) { + throw e; + } + + assertThat(e).isInstanceOf(testCase.expectedException); + } + } + + @Test + public void s3Presigner_endpointSigningRegionAndServiceNamesAreCorrect() { + try { + if (getObjectRequest != null) { + PresignedGetObjectRequest presignedGetObjectRequest = + s3Presigner.presignGetObject(r -> r.getObjectRequest(getObjectRequest) + .signatureDuration(Duration.ofDays(1))); + + URI uriWithoutQueryParameters = presignedGetObjectRequest.httpRequest() + .copy(r -> r.removeQueryParameter("X-Amz-SignedHeaders")) + .getUri(); + assertThat(uriWithoutQueryParameters).isEqualTo(testCase.expectedEndpoint); + assertThat(signingRegion()).isEqualTo(testCase.expectedSigningRegion); + assertThat(signingServiceName()).isEqualTo(testCase.expectedSigningServiceName); + } else { + System.out.println("There are (currently) no operations which do not take a bucket. Test will be skipped."); + } + + assertThat(testCase.expectedException).isNull(); + } catch (RuntimeException e) { + if (testCase.expectedException == null) { + throw e; + } + + assertThat(e).isInstanceOf(testCase.expectedException); + } + } + + @Test + public void s3Utilities_endpointSigningRegionAndServiceNamesAreCorrect() throws URISyntaxException { + try { + if (testCase.getObjectBucketName != null) { + URL url = s3Utilities.getUrl(r -> r.bucket(testCase.getObjectBucketName) + .key("object") + .endpoint(testCase.endpointUrl) + .region(testCase.clientRegion)); + assertThat(url.toURI()).isEqualTo(testCase.expectedEndpoint); + } else { + System.out.println("There are (currently) no operations which do not take a bucket. Test will be skipped."); + } + + assertThat(testCase.expectedException).isNull(); + } catch (RuntimeException e) { + if (testCase.expectedException == null) { + throw e; + } + + assertThat(e).isInstanceOf(testCase.expectedException); + } + } + + private String signingServiceName() { + return attributesPassedToSigner().getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME); + } + + private Region signingRegion() { + return attributesPassedToSigner().getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION); + } + + private ExecutionAttributes attributesPassedToSigner() { + ArgumentCaptor executionAttributes = ArgumentCaptor.forClass(ExecutionAttributes.class); + Mockito.verify(mockSigner, Mockito.atLeast(0)).sign(any(), executionAttributes.capture()); + + if (executionAttributes.getAllValues().isEmpty()) { + return attributesPassedToPresigner(); + } + + return executionAttributes.getValue(); + } + + private ExecutionAttributes attributesPassedToPresigner() { + ArgumentCaptor executionAttributes = ArgumentCaptor.forClass(ExecutionAttributes.class); + Mockito.verify(mockSigner).presign(any(), executionAttributes.capture()); + return executionAttributes.getValue(); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection testCases() { + List cases = new ArrayList<>(); + + cases.add(new TestCase().setCaseName("normal bucket") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://beta.example.com/bucketname/object") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("normal bucket with http, path, query and port") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://beta.example.com:1234/path/bucketname/object?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("access point") + .setGetObjectBucketName("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint") + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://myendpoint-123456789012.beta.example.com/object") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("access point with http, path, query, and port") + .setGetObjectBucketName("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint") + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://myendpoint-123456789012.beta.example.com:1234/path/object?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("outposts access point") + .setGetObjectBucketName("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint") + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://myaccesspoint-123456789012.op-01234567890123456.beta.example.com/object") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("outposts access point with http, path, query, and port") + .setGetObjectBucketName("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint") + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://myaccesspoint-123456789012.op-01234567890123456.beta.example.com:1234/path/object?foo=bar") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("list buckets") + .setEndpointUrl("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com/") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("list buckets with http, path, query and port") + .setEndpointUrl("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path/?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("normal bucket with vpce, path style addressing explicitly enabled") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com") + .setS3Configuration(c -> c.pathStyleAccessEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com/bucketname/object") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("normal bucket with http, vpce, path, query, port, and path style addressing explicitly enabled") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path?foo=bar") + .setS3Configuration(c -> c.pathStyleAccessEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path/bucketname/object?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("normal bucket with vpce") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com/bucketname/object") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("normal bucket with http, vpce, path, query and port") + .setGetObjectBucketName("bucketname") + .setEndpointUrl("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://bucket.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path/bucketname/object?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("access point with different arn region and arn region enabled") + .setGetObjectBucketName("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint") + .setEndpointUrl("https://accesspoint.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com") + .setS3Configuration(c -> c.useArnRegionEnabled(true)) + .setClientRegion(Region.EU_WEST_1) + .setExpectedEndpoint("https://myendpoint-123456789012.accesspoint.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com/object") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("access point with http, path, query, port, different arn region, and arn region enabled") + .setGetObjectBucketName("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint") + .setEndpointUrl("http://accesspoint.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path?foo=bar") + .setS3Configuration(c -> c.useArnRegionEnabled(true)) + .setClientRegion(Region.EU_WEST_1) + .setExpectedEndpoint("http://myendpoint-123456789012.accesspoint.vpce-123-abc.s3.us-west-2.vpce.amazonaws.com:1234/path/object?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("outposts access point with dual stack enabled") + .setGetObjectBucketName("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint") + .setEndpointUrl("https://beta.example.com") + .setS3Configuration(c -> c.dualstackEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedException(IllegalArgumentException.class)); + + return cases; + } + + private static class TestCase { + private String caseName; + private URI endpointUrl; + private String getObjectBucketName; + private S3Configuration s3Configuration = S3Configuration.builder().build(); + private Region clientRegion; + private URI expectedEndpoint; + private String expectedSigningServiceName; + private Region expectedSigningRegion; + private Class expectedException; + + public TestCase setCaseName(String caseName) { + this.caseName = caseName; + return this; + } + + public TestCase setGetObjectBucketName(String getObjectBucketName) { + this.getObjectBucketName = getObjectBucketName; + return this; + } + + public TestCase setEndpointUrl(String endpointUrl) { + this.endpointUrl = URI.create(endpointUrl); + return this; + } + + public TestCase setS3Configuration(Consumer s3Configuration) { + S3Configuration.Builder configBuilder = S3Configuration.builder(); + s3Configuration.accept(configBuilder); + this.s3Configuration = configBuilder.build(); + return this; + } + + public TestCase setClientRegion(Region clientRegion) { + this.clientRegion = clientRegion; + return this; + } + + public TestCase setExpectedEndpoint(String expectedEndpoint) { + this.expectedEndpoint = URI.create(expectedEndpoint); + return this; + } + + public TestCase setExpectedSigningServiceName(String expectedSigningServiceName) { + this.expectedSigningServiceName = expectedSigningServiceName; + return this; + } + + public TestCase setExpectedSigningRegion(Region expectedSigningRegion) { + this.expectedSigningRegion = expectedSigningRegion; + return this; + } + + public TestCase setExpectedException(Class expectedException) { + this.expectedException = expectedException; + return this; + } + + @Override + public String toString() { + return this.caseName + (getObjectBucketName == null ? "" : ": " + getObjectBucketName); + } + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/InvalidRegionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/InvalidRegionTest.java index 82cc321f2bd6..467f949301dd 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/InvalidRegionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/InvalidRegionTest.java @@ -59,7 +59,7 @@ public void invalidS3ArnRegionAtRequestGivesHelpfulMessage() { .key("test"))) .isInstanceOf(SdkClientException.class) .hasMessageContaining("US_EAST_1") - .hasMessageContaining("region"); + .hasMessageContaining("URI"); } @Test @@ -84,6 +84,6 @@ public void invalidS3PresignerArnRegionAtRequestGivesHelpfulMessage() { .signatureDuration(Duration.ofMinutes(15)))) .isInstanceOf(SdkClientException.class) .hasMessageContaining("US_EAST_1") - .hasMessageContaining("region"); + .hasMessageContaining("URI"); } } 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 2da7f5ceeaa8..d7f452343688 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 @@ -123,18 +123,6 @@ public void accessPointArn_correctlyRewritesEndpoint() throws Exception { assertEndpointMatches(mockHttpClient.getLastRequest(), customEndpoint.toString()); } - @Test - public void accessPointArn_customEndpoint_throwsIllegalArgumentException() throws Exception { - URI customEndpoint = URI.create("https://foobar.amazonaws.com"); - mockHttpClient.stubNextResponse(mockListObjectsResponse()); - S3Client s3Client = clientBuilder().endpointOverride(customEndpoint).build(); - String accessPointArn = "arn:aws:s3:ap-south-1:12345678910:accesspoint:foobar"; - - assertThatThrownBy(() -> s3Client.listObjects(ListObjectsRequest.builder().bucket(accessPointArn).build())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("endpoint override"); - } - @Test public void accessPointArn_differentRegion_useArnRegionFalse_throwsIllegalArgumentException() throws Exception { mockHttpClient.stubNextResponse(mockListObjectsResponse()); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContextTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContextTest.java index 9b846ad9cda2..33d145fa6cbb 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContextTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/S3EndpointResolverContextTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.net.URI; import org.junit.Test; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; @@ -32,7 +33,6 @@ public class S3EndpointResolverContextTest { @Test public void toBuilder_minimal() { S3EndpointResolverContext context = S3EndpointResolverContext.builder().build(); - assertFalse(context.endpointOverridden()); assertNull(context.originalRequest()); assertNull(context.region()); assertNull(context.serviceConfiguration()); @@ -43,14 +43,15 @@ public void toBuilder_minimal() { public void toBuilder_maximal() { S3Configuration serviceConfiguration = S3Configuration.builder().build(); SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder().protocol("http").host("host").method(SdkHttpMethod.POST).build(); + URI endpoint = URI.create("https://endpoint.com"); S3EndpointResolverContext context = S3EndpointResolverContext.builder() - .endpointOverridden(true) + .endpointOverride(endpoint) .originalRequest(PutObjectRequest.builder().build()) .region(Region.US_EAST_1) .serviceConfiguration(serviceConfiguration) .request(httpRequest) .build(); - assertTrue(context.endpointOverridden()); + assertThat(context.endpointOverride()).isEqualTo(endpoint); assertThat(context.originalRequest()).isInstanceOf(PutObjectRequest.class); assertThat(context.region()).isEqualTo(Region.US_EAST_1); assertThat(context.serviceConfiguration()).isEqualTo(serviceConfiguration); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/resource/OutpostAccessPointArnEndpointResolutionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/resource/OutpostAccessPointArnEndpointResolutionTest.java index 95cef1ba878d..5bbd2ec17023 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/resource/OutpostAccessPointArnEndpointResolutionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/resource/OutpostAccessPointArnEndpointResolutionTest.java @@ -61,18 +61,6 @@ public void outpostArn_correctlyRewritesEndpoint() throws Exception { assertEndpointMatches(mockHttpClient.getLastRequest(), customEndpoint.toString()); } - @Test - public void outpostArn_customEndpoint_throwsIllegalArgumentException() throws Exception { - URI customEndpoint = URI.create("https://foobar.amazonaws.com"); - mockHttpClient.stubNextResponse(mockListObjectsResponse()); - S3Client s3Client = clientBuilder().endpointOverride(customEndpoint).build(); - String outpostArn = "arn:aws:s3-outposts:ap-south-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"; - - assertThatThrownBy(() -> s3Client.listObjects(ListObjectsRequest.builder().bucket(outpostArn).build())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("endpoint override"); - } - @Test public void outpostArn_dualstackEnabled_throwsIllegalArgumentException() throws Exception { mockHttpClient.stubNextResponse(mockListObjectsResponse()); diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/ArnHandler.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/ArnHandler.java deleted file mode 100644 index 6a1376201c00..000000000000 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/ArnHandler.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.services.s3control.internal; - -import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME; -import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SIGNING_REGION; -import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.ENDPOINT_OVERRIDDEN; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.S3_OUTPOSTS; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isDualstackEnabled; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isFipsEnabledInClientConfig; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isFipsRegionProvided; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isUseArnRegionEnabledInClientConfig; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.arns.Arn; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.regions.PartitionMetadata; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.internal.resource.S3OutpostResource; -import software.amazon.awssdk.services.s3.internal.resource.S3Resource; -import software.amazon.awssdk.services.s3.internal.usearnregion.UseArnRegionProviderChain; -import software.amazon.awssdk.services.s3control.S3ControlConfiguration; - -@SdkInternalApi -public final class ArnHandler { - private static final String X_AMZ_OUTPOST_ID_HEADER = "x-amz-outpost-id"; - private static final ArnHandler INSTANCE = new ArnHandler(); - private static final UseArnRegionProviderChain USE_ARN_REGION_RESOLVER = UseArnRegionProviderChain.create(); - - private ArnHandler() { - } - - public static ArnHandler getInstance() { - return INSTANCE; - } - - public SdkHttpRequest resolveHostForArn(SdkHttpRequest request, - S3ControlConfiguration configuration, - Arn arn, - ExecutionAttributes executionAttributes) { - - S3Resource s3Resource = S3ControlArnConverter.getInstance().convertArn(arn); - - String clientRegion = executionAttributes.getAttribute(SIGNING_REGION).id(); - String originalArnRegion = s3Resource.region().orElseThrow(() -> new IllegalArgumentException("Region is missing")); - - boolean isFipsEnabled = isFipsEnabledInClientConfig(configuration) || isFipsRegionProvided(clientRegion, - originalArnRegion, - useArnRegion(configuration)); - - String arnRegion = removeFipsIfNeeded(originalArnRegion); - validateConfiguration(executionAttributes, arn.partition(), arnRegion, configuration); - - executionAttributes.putAttribute(SIGNING_REGION, Region.of(arnRegion)); - - S3Resource parentS3Resource = s3Resource.parentS3Resource().orElse(null); - if (parentS3Resource instanceof S3OutpostResource) { - return handleOutpostArn(request, (S3OutpostResource) parentS3Resource, isFipsEnabled, configuration, - executionAttributes); - } else { - throw new IllegalArgumentException("Parent resource invalid, outpost resource expected."); - } - - } - - private SdkHttpRequest handleOutpostArn(SdkHttpRequest request, - S3OutpostResource outpostResource, - boolean isFipsEnabled, - S3ControlConfiguration configuration, - ExecutionAttributes executionAttributes) { - if (isFipsEnabled) { - throw new IllegalArgumentException("FIPS endpoints are not supported for outpost ARNs"); - } - - if (isDualstackEnabled(configuration)) { - throw new IllegalArgumentException("Dualstack endpoints are not supported for outpost ARNs"); - } - - executionAttributes.putAttribute(SERVICE_SIGNING_NAME, S3_OUTPOSTS); - - SdkHttpRequest.Builder requestBuilder = request.toBuilder().appendHeader(X_AMZ_OUTPOST_ID_HEADER, - outpostResource.outpostId()); - String arnRegion = outpostResource.region().orElseThrow(() -> new IllegalArgumentException("arn region is missing")); - String dnsSuffix = PartitionMetadata.of(Region.of(arnRegion)).dnsSuffix(); - - String host = String.format("s3-outposts.%s.%s", arnRegion, dnsSuffix); - return requestBuilder.host(host).build(); - } - - private void validateConfiguration(ExecutionAttributes executionAttributes, String arnPartition, String arnRegion, - S3ControlConfiguration configuration) { - String clientRegionString = removeFipsIfNeeded(executionAttributes.getAttribute(SIGNING_REGION).id()); - Region clientRegion = Region.of(clientRegionString); - - if (Boolean.TRUE.equals(executionAttributes.getAttribute(ENDPOINT_OVERRIDDEN))) { - throw new IllegalArgumentException("An ARN cannot be passed to an " - + " operation if the client has been configured with an endpoint " - + "override."); - } - String clientPartition = PartitionMetadata.of(clientRegion).id(); - - if (!arnPartition.equals(clientPartition)) { - throw new IllegalArgumentException("The partition field of the ARN being passed as a bucket parameter to " - + "an S3 operation does not match the partition the client has been configured " - + "with. Provided " - + "partition: '" + arnPartition + "'; client partition: " - + "'" + clientPartition + "'."); - } - - if (!arnRegion.equals(clientRegionString) && !useArnRegion(configuration)) { - throw new IllegalArgumentException("The region field of the ARN being passed as a bucket parameter to an " - + "operation does not match the region the client was configured " - + "with. Provided region: '" + arnRegion + "'; client " - + "region: '" + clientRegionString + "'."); - } - } - - private String removeFipsIfNeeded(String region) { - if (region.startsWith("fips-")) { - return region.replace("fips-", ""); - } - - if (region.endsWith("-fips")) { - return region.replace("-fips", ""); - } - return region; - } - - private boolean useArnRegion(S3ControlConfiguration configuration) { - // If useArnRegion is false, it was not set to false by the customer, it was simply not enabled - if (isUseArnRegionEnabledInClientConfig(configuration)) { - return true; - } - - return USE_ARN_REGION_RESOLVER.resolveUseArnRegion().orElse(false); - } -} diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/HandlerUtils.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/HandlerUtils.java index 5743ac39aa52..6c191d63fb7e 100644 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/HandlerUtils.java +++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/HandlerUtils.java @@ -43,7 +43,8 @@ public static boolean isUseArnRegionEnabledInClientConfig(S3ControlConfiguration /** * Returns whether a FIPS pseudo region is provided. */ - public static boolean isFipsRegionProvided(String clientRegion, String arnRegion, boolean useArnRegion) { + public static boolean isFipsRegion(String clientRegion, String arnRegion, + S3ControlConfiguration serviceConfig, boolean useArnRegion) { if (useArnRegion) { return isFipsRegion(arnRegion); } diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/S3ControlInternalExecutionAttribute.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/S3ControlInternalExecutionAttribute.java index 407b64f67e97..ee7680990da9 100644 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/S3ControlInternalExecutionAttribute.java +++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/S3ControlInternalExecutionAttribute.java @@ -15,11 +15,11 @@ package software.amazon.awssdk.services.s3control.internal; -import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.interceptor.ExecutionAttribute; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; -@SdkProtectedApi +@SdkInternalApi public final class S3ControlInternalExecutionAttribute extends SdkExecutionAttribute { /** diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptor.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptor.java index 66589f922d03..4725d1a1c675 100644 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptor.java +++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptor.java @@ -18,30 +18,37 @@ import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME; import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SIGNING_REGION; +import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.CLIENT_ENDPOINT; import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.ENDPOINT_OVERRIDDEN; import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.ENDPOINT_PREFIX; import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.S3_OUTPOSTS; import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isDualstackEnabled; import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isFipsEnabledInClientConfig; import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isFipsRegion; +import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isFipsRegion; +import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.isUseArnRegionEnabledInClientConfig; import static software.amazon.awssdk.services.s3control.internal.S3ControlInternalExecutionAttribute.S3_ARNABLE_FIELD; +import java.net.URI; +import java.util.Optional; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.arns.Arn; import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.interceptor.Context; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.regions.PartitionMetadata; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.internal.resource.S3OutpostResource; +import software.amazon.awssdk.services.s3.internal.resource.S3Resource; +import software.amazon.awssdk.services.s3.internal.usearnregion.UseArnRegionProviderChain; import software.amazon.awssdk.services.s3control.S3ControlConfiguration; -import software.amazon.awssdk.services.s3control.internal.ArnHandler; import software.amazon.awssdk.services.s3control.internal.S3ArnableField; -import software.amazon.awssdk.services.s3control.model.CreateBucketRequest; -import software.amazon.awssdk.services.s3control.model.ListRegionalBucketsRequest; +import software.amazon.awssdk.services.s3control.internal.S3ControlArnConverter; import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.Validate; /** * Execution interceptor which modifies the HTTP request to S3 Control to @@ -50,93 +57,171 @@ */ @SdkInternalApi public final class EndpointAddressInterceptor implements ExecutionInterceptor { - private final ArnHandler arnHandler; - - public EndpointAddressInterceptor() { - arnHandler = ArnHandler.getInstance(); - } + private static final String X_AMZ_OUTPOST_ID_HEADER = "x-amz-outpost-id"; + private static final UseArnRegionProviderChain USE_ARN_REGION_RESOLVER = UseArnRegionProviderChain.create(); @Override public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { - SdkHttpRequest request = context.httpRequest(); - - S3ControlConfiguration config = (S3ControlConfiguration) executionAttributes.getAttribute( - AwsSignerExecutionAttribute.SERVICE_CONFIG); + Optional requestArn = getRequestArn(executionAttributes); - S3ArnableField arnableField = executionAttributes.getAttribute(S3_ARNABLE_FIELD); - - if (arnableField != null && arnableField.arn() != null) { - return arnHandler.resolveHostForArn(request, config, arnableField.arn(), executionAttributes); + if (requestArn.isPresent()) { + return resolveHostForOutpostArnRequest(context.httpRequest(), executionAttributes, requestArn.get()); + } else if (isNonArnOutpostRequest(context.request())) { + return resolveHostForOutpostNonArnRequest(context.httpRequest(), executionAttributes); + } else { + return resolveHostForNonOutpostNonArnRequest(context.httpRequest(), executionAttributes); } + } + + private SdkHttpRequest resolveHostForOutpostArnRequest(SdkHttpRequest request, + ExecutionAttributes executionAttributes, + Arn arn) { + S3Resource s3Resource = S3ControlArnConverter.getInstance().convertArn(arn); + + S3ControlConfiguration serviceConfig = getServiceConfig(executionAttributes); + String signingRegion = executionAttributes.getAttribute(SIGNING_REGION).id(); + String arnRegion = s3Resource.region().orElseThrow(() -> new IllegalArgumentException("Region is missing from ARN.")); + String arnPartion = arn.partition(); + S3Resource parentS3Resource = s3Resource.parentS3Resource().orElse(null); + + Validate.isTrue(!willCallFipsRegion(signingRegion, arnRegion, serviceConfig), + "FIPS is not supported for outpost requests."); + + // Even though we validated that we're not *calling* a FIPS region, the client region may still be a FIPS region if we're + // using the ARN region. For that reason, we need to strip off the "fips" from the signing region before we get the + // partition to make sure we're not making a cross-partition call. + signingRegion = removeFipsIfNeeded(signingRegion); + + String signingPartition = PartitionMetadata.of(Region.of(signingRegion)).id(); + + S3OutpostResource outpostResource = Validate.isInstanceOf(S3OutpostResource.class, parentS3Resource, + "The ARN passed must have a parent outpost resource."); + Validate.isTrue(!isDualstackEnabled(serviceConfig), "Dual stack endpoints are not supported for outpost requests."); + Validate.isTrue(arnPartion.equals(signingPartition), + "The partition field of the ARN being passed as a bucket parameter to an S3 operation does not match " + + "the partition the client has been configured with. Provided partition: '%s'; client partition: '%s'.", + arnPartion, signingPartition); + Validate.isTrue(useArnRegion(serviceConfig) || arnRegion.equals(signingRegion), + "The region field of the ARN being passed as a bucket parameter to an operation does not match the " + + "region the client was configured with. Provided region: '%s'; client region: '%s'.", + arnRegion, signingRegion); + + executionAttributes.putAttribute(SIGNING_REGION, Region.of(arnRegion)); + executionAttributes.putAttribute(SERVICE_SIGNING_NAME, S3_OUTPOSTS); - String host; + SdkHttpRequest.Builder requestBuilder = request.toBuilder() + .appendHeader(X_AMZ_OUTPOST_ID_HEADER, outpostResource.outpostId()); - // If the request is an non-arn outpost request - if (isNonArnOutpostRequest(context.request())) { - host = resolveHostForNonArnOutpostRequest(config, executionAttributes); + if (isEndpointOverridden(executionAttributes)) { + // Drop endpoint prefix for ARN-based requests + requestBuilder.host(endpointOverride(executionAttributes).getHost()); } else { - host = resolveHost(request, config); + String arnPartitionDnsSuffix = PartitionMetadata.of(arnPartion).dnsSuffix(); + requestBuilder.host(String.format("s3-outposts.%s.%s", arnRegion, arnPartitionDnsSuffix)); } - return request.toBuilder() - .host(host) - .build(); + return requestBuilder.build(); } - private String resolveHostForNonArnOutpostRequest(S3ControlConfiguration configuration, - ExecutionAttributes executionAttributes) { - if (Boolean.TRUE.equals(executionAttributes.getAttribute(ENDPOINT_OVERRIDDEN))) { - throw new IllegalArgumentException("Endpoint must not be overridden"); + private SdkHttpRequest resolveHostForOutpostNonArnRequest(SdkHttpRequest sdkHttpRequest, + ExecutionAttributes executionAttributes) { + S3ControlConfiguration serviceConfig = getServiceConfig(executionAttributes); + Region signingRegion = executionAttributes.getAttribute(SIGNING_REGION); + + Validate.isTrue(!isDualstackEnabled(serviceConfig), + "Dual stack is not supported for outpost requests."); + Validate.isTrue(!isFipsEnabledInClientConfig(serviceConfig) && !isFipsRegion(signingRegion.id()), + "FIPS endpoints are not supported for outpost requests."); + + executionAttributes.putAttribute(SERVICE_SIGNING_NAME, S3_OUTPOSTS); + + if (isEndpointOverridden(executionAttributes)) { + // Preserve endpoint prefix for endpoint-overridden non-ARN-based requests + return sdkHttpRequest; + } else { + String signingDnsSuffix = PartitionMetadata.of(signingRegion).dnsSuffix(); + return sdkHttpRequest.copy(r -> r.host(String.format("s3-outposts.%s.%s", signingRegion, signingDnsSuffix))); } + } - if (isDualstackEnabled(configuration)) { - throw new IllegalArgumentException("Dualstack endpoints are not supported"); + private SdkHttpRequest resolveHostForNonOutpostNonArnRequest(SdkHttpRequest request, + ExecutionAttributes executionAttributes) { + S3ControlConfiguration serviceConfig = getServiceConfig(executionAttributes); + + boolean isDualStackEnabled = isDualstackEnabled(serviceConfig); + boolean isFipsEnabledInClient = isFipsEnabledInClientConfig(serviceConfig); + + Validate.isTrue(!isDualStackEnabled || !isFipsEnabledInClient, "Dual stack and FIPS are not supported together."); + + if (isEndpointOverridden(executionAttributes)) { + Validate.isTrue(!isDualStackEnabled, "Dual stack is not supported with endpoint overrides."); + Validate.isTrue(!isFipsEnabledInClient, "FIPS is not supported with endpoint overrides."); + // Preserve endpoint prefix for endpoint-overridden non-ARN-based requests + return request; + } else if (isDualStackEnabled) { + String newEndpointPrefix = String.format("%s.%s", ENDPOINT_PREFIX, "dualstack"); + return request.copy(r -> r.host(request.host().replace(ENDPOINT_PREFIX, newEndpointPrefix))); + } else if (isFipsEnabledInClient) { + String newEndpointPrefix = String.format("%s-%s", ENDPOINT_PREFIX, "fips"); + return request.copy(r -> r.host(request.host().replace(ENDPOINT_PREFIX, newEndpointPrefix))); + } else { + return request; } + } + + private Optional getRequestArn(ExecutionAttributes executionAttributes) { + return Optional.ofNullable(executionAttributes.getAttribute(S3_ARNABLE_FIELD)) + .map(S3ArnableField::arn); + } + + private boolean isNonArnOutpostRequest(SdkRequest request) { + return request.getValueForField("OutpostId", String.class) + .map(StringUtils::isNotBlank) + .orElse(false); + } + + private S3ControlConfiguration getServiceConfig(ExecutionAttributes executionAttributes) { + return (S3ControlConfiguration) executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_CONFIG); + } - Region region = executionAttributes.getAttribute(SIGNING_REGION); - if (isFipsEnabledInClientConfig(configuration) || isFipsRegion(region.id())) { - throw new IllegalArgumentException("FIPS endpoints are not supported"); + private boolean useArnRegion(S3ControlConfiguration configuration) { + // If useArnRegion is false, it was not set to false by the customer, it was simply not enabled + if (isUseArnRegionEnabledInClientConfig(configuration)) { + return true; } - executionAttributes.putAttribute(SERVICE_SIGNING_NAME, S3_OUTPOSTS); + return USE_ARN_REGION_RESOLVER.resolveUseArnRegion().orElse(false); + } - String dnsSuffix = PartitionMetadata.of(region).dnsSuffix(); + private boolean isEndpointOverridden(ExecutionAttributes executionAttributes) { + return Boolean.TRUE.equals(executionAttributes.getAttribute(ENDPOINT_OVERRIDDEN)); + } - return String.format("s3-outposts.%s.%s", region, dnsSuffix); + private URI endpointOverride(ExecutionAttributes executionAttributes) { + return executionAttributes.getAttribute(CLIENT_ENDPOINT); } - /** - * It should redirect signer if the request is CreateBucketRequest or ListRegionalBucketsRequest with outpostId present - */ - private boolean isNonArnOutpostRequest(SdkRequest request) { - if (request instanceof CreateBucketRequest && (StringUtils.isNotBlank(((CreateBucketRequest) request).outpostId()))) { + private boolean willCallFipsRegion(String signingRegion, String arnRegion, S3ControlConfiguration serviceConfig) { + if (useArnRegion(serviceConfig)) { + return isFipsRegion(arnRegion); + } + + if (serviceConfig.fipsModeEnabled()) { return true; } - return request instanceof ListRegionalBucketsRequest && - (StringUtils.isNotBlank(((ListRegionalBucketsRequest) request).outpostId())); + return isFipsRegion(signingRegion); } - private String resolveHost(SdkHttpRequest request, S3ControlConfiguration configuration) { - if (isDualstackEnabled(configuration) && isFipsEnabledInClientConfig(configuration)) { - throw SdkClientException.create("Cannot use both Dual-Stack endpoints and FIPS endpoints"); + private String removeFipsIfNeeded(String region) { + if (region.startsWith("fips-")) { + return region.replace("fips-", ""); } - String host = request.getUri().getHost(); - if (isDualstackEnabled(configuration)) { - if (!host.contains(ENDPOINT_PREFIX)) { - throw SdkClientException.create(String.format("The Dual-Stack option cannot be used with custom endpoints (%s)", - request.getUri())); - } - host = host.replace(ENDPOINT_PREFIX, String.format("%s.%s", ENDPOINT_PREFIX, "dualstack")); - } else if (isFipsEnabledInClientConfig(configuration)) { - if (!host.contains(ENDPOINT_PREFIX)) { - throw SdkClientException.create(String.format("The FIPS option cannot be used with custom endpoints (%s)", - request.getUri())); - } - host = host.replace(ENDPOINT_PREFIX, String.format("%s-%s", ENDPOINT_PREFIX, "fips")); + if (region.endsWith("-fips")) { + return region.replace("-fips", ""); } - return host; + return region; } } diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/EndpointOverrideEndpointResolutionTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/EndpointOverrideEndpointResolutionTest.java new file mode 100644 index 000000000000..057c5b151f53 --- /dev/null +++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/EndpointOverrideEndpointResolutionTest.java @@ -0,0 +1,310 @@ +/* + * 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.services.s3control; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.signer.Presigner; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3control.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3control.model.GetAccessPointRequest; +import software.amazon.awssdk.services.s3control.model.GetBucketRequest; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +@RunWith(Parameterized.class) +public class EndpointOverrideEndpointResolutionTest { + private final TestCase testCase; + private final SignerAndPresigner mockSigner; + private final MockSyncHttpClient mockHttpClient; + private final S3ControlClient s3ControlClient; + + public interface SignerAndPresigner extends Signer, Presigner {} + + public EndpointOverrideEndpointResolutionTest(TestCase testCase) { + this.testCase = testCase; + this.mockSigner = Mockito.mock(SignerAndPresigner.class); + this.mockHttpClient = new MockSyncHttpClient(); + this.s3ControlClient = + S3ControlClient.builder() + .region(testCase.clientRegion) + .endpointOverride(testCase.endpointUrl) + .serviceConfiguration(testCase.s3ControlConfiguration) + .httpClient(mockHttpClient) + .overrideConfiguration(c -> c.putAdvancedOption(SdkAdvancedClientOption.SIGNER, mockSigner)) + .build(); + + HttpExecuteResponse response = + HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + mockHttpClient.stubNextResponse(response); + + Mockito.when(mockSigner.sign(any(), any())).thenAnswer(r -> r.getArgumentAt(0, SdkHttpFullRequest.class)); + Mockito.when(mockSigner.presign(any(), any())).thenAnswer(r -> r.getArgumentAt(0, SdkHttpFullRequest.class) + .copy(h -> h.putRawQueryParameter("X-Amz-SignedHeaders", "host"))); + } + + @Test + public void s3ControlClient_endpointSigningRegionAndServiceNamesAreCorrect() { + try { + if (testCase.getAccessPointRequest != null) { + s3ControlClient.getAccessPoint(testCase.getAccessPointRequest); + } else if (testCase.createBucketRequest != null) { + s3ControlClient.createBucket(testCase.createBucketRequest); + } else { + s3ControlClient.getBucket(testCase.getBucketRequest); + } + + assertThat(testCase.expectedException).isNull(); + assertThat(mockHttpClient.getLastRequest().getUri()).isEqualTo(testCase.expectedEndpoint); + assertThat(signingRegion()).isEqualTo(testCase.expectedSigningRegion); + assertThat(signingServiceName()).isEqualTo(testCase.expectedSigningServiceName); + } catch (RuntimeException e) { + if (testCase.expectedException == null) { + throw e; + } + + assertThat(e).isInstanceOf(testCase.expectedException); + } + } + + private String signingServiceName() { + return attributesPassedToSigner().getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME); + } + + private Region signingRegion() { + return attributesPassedToSigner().getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION); + } + + private ExecutionAttributes attributesPassedToSigner() { + ArgumentCaptor executionAttributes = ArgumentCaptor.forClass(ExecutionAttributes.class); + Mockito.verify(mockSigner, Mockito.atLeast(0)).sign(any(), executionAttributes.capture()); + + if (executionAttributes.getAllValues().isEmpty()) { + return attributesPassedToPresigner(); + } + + return executionAttributes.getValue(); + } + + private ExecutionAttributes attributesPassedToPresigner() { + ArgumentCaptor executionAttributes = ArgumentCaptor.forClass(ExecutionAttributes.class); + Mockito.verify(mockSigner).presign(any(), executionAttributes.capture()); + return executionAttributes.getValue(); + } + + @Parameterized.Parameters(name = "{0}") + public static Collection testCases() { + List cases = new ArrayList<>(); + + cases.add(new TestCase().setCaseName("get-access-point by access point name") + .setGetAccessPointRequest(r -> r.name("apname").accountId("123456789012")) + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://123456789012.beta.example.com/v20180820/accesspoint/apname") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-access-point by access point name with http, path, query and port") + .setGetAccessPointRequest(r -> r.name("apname").accountId("123456789012")) + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://123456789012.beta.example.com:1234/path/v20180820/accesspoint/apname?foo=bar") + .setExpectedSigningServiceName("s3") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-access-point by outpost access point arn") + .setGetAccessPointRequest(r -> r.name("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint")) + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://beta.example.com/v20180820/accesspoint/myaccesspoint") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-access-point by outpost access point arn with http, path, query and port") + .setGetAccessPointRequest(r -> r.name("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint")) + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://beta.example.com:1234/path/v20180820/accesspoint/myaccesspoint?foo=bar") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("create-bucket with outpost ID") + .setCreateBucketRequest(r -> r.bucket("bucketname").outpostId("op-123")) + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://beta.example.com/v20180820/bucket/bucketname") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("create-bucket with outpost ID, http, path, query and port") + .setCreateBucketRequest(r -> r.bucket("bucketname").outpostId("op-123")) + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://beta.example.com:1234/path/v20180820/bucket/bucketname?foo=bar") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-bucket with outpost bucket arn") + .setGetBucketRequest(r -> r.bucket("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket")) + .setEndpointUrl("https://beta.example.com") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("https://beta.example.com/v20180820/bucket/mybucket") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-bucket with outpost bucket arn, http, path, query and port") + .setGetBucketRequest(r -> r.bucket("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket")) + .setEndpointUrl("http://beta.example.com:1234/path?foo=bar") + .setClientRegion(Region.US_WEST_2) + .setExpectedEndpoint("http://beta.example.com:1234/path/v20180820/bucket/mybucket?foo=bar") + .setExpectedSigningServiceName("s3-outposts") + .setExpectedSigningRegion(Region.US_WEST_2)); + + cases.add(new TestCase().setCaseName("get-access-point by access point name with dualstack") + .setGetAccessPointRequest(r -> r.name("apname").accountId("123456789012")) + .setEndpointUrl("https://beta.example.com") + .setS3ControlConfiguration(c -> c.dualstackEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedException(IllegalArgumentException.class)); + + cases.add(new TestCase().setCaseName("get-access-point by outpost access point arn with dualstack") + .setGetAccessPointRequest(r -> r.name("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint")) + .setEndpointUrl("https://beta.example.com") + .setS3ControlConfiguration(c -> c.dualstackEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedException(IllegalArgumentException.class)); + + cases.add(new TestCase().setCaseName("create-bucket with outpost ID with dualstack") + .setCreateBucketRequest(r -> r.bucket("bucketname").outpostId("op-123")) + .setEndpointUrl("https://beta.example.com") + .setS3ControlConfiguration(c -> c.dualstackEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedException(IllegalArgumentException.class)); + + cases.add(new TestCase().setCaseName("get-bucket with outpost bucket arn with dualstack") + .setGetBucketRequest(r -> r.bucket("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket")) + .setEndpointUrl("https://beta.example.com") + .setS3ControlConfiguration(c -> c.dualstackEnabled(true)) + .setClientRegion(Region.US_WEST_2) + .setExpectedException(IllegalArgumentException.class)); + + return cases; + } + + private static class TestCase { + private String caseName; + private URI endpointUrl; + private GetAccessPointRequest getAccessPointRequest; + private CreateBucketRequest createBucketRequest; + private GetBucketRequest getBucketRequest; + private S3ControlConfiguration s3ControlConfiguration = S3ControlConfiguration.builder().build(); + private Region clientRegion; + private URI expectedEndpoint; + private String expectedSigningServiceName; + private Region expectedSigningRegion; + private Class expectedException; + + public TestCase setCaseName(String caseName) { + this.caseName = caseName; + return this; + } + + public TestCase setGetAccessPointRequest(Consumer consumer) { + GetAccessPointRequest.Builder builder = GetAccessPointRequest.builder(); + consumer.accept(builder); + this.getAccessPointRequest = builder.build(); + return this; + } + + public TestCase setCreateBucketRequest(Consumer consumer) { + CreateBucketRequest.Builder builder = CreateBucketRequest.builder(); + consumer.accept(builder); + this.createBucketRequest = builder.build(); + return this; + } + + public TestCase setGetBucketRequest(Consumer consumer) { + GetBucketRequest.Builder builder = GetBucketRequest.builder(); + consumer.accept(builder); + this.getBucketRequest = builder.build(); + return this; + } + + public TestCase setEndpointUrl(String endpointUrl) { + this.endpointUrl = URI.create(endpointUrl); + return this; + } + + public TestCase setS3ControlConfiguration(Consumer s3ControlConfiguration) { + S3ControlConfiguration.Builder configBuilder = S3ControlConfiguration.builder(); + s3ControlConfiguration.accept(configBuilder); + this.s3ControlConfiguration = configBuilder.build(); + return this; + } + + public TestCase setClientRegion(Region clientRegion) { + this.clientRegion = clientRegion; + return this; + } + + public TestCase setExpectedEndpoint(String expectedEndpoint) { + this.expectedEndpoint = URI.create(expectedEndpoint); + return this; + } + + public TestCase setExpectedSigningServiceName(String expectedSigningServiceName) { + this.expectedSigningServiceName = expectedSigningServiceName; + return this; + } + + public TestCase setExpectedSigningRegion(Region expectedSigningRegion) { + this.expectedSigningRegion = expectedSigningRegion; + return this; + } + + public TestCase setExpectedException(Class expectedException) { + this.expectedException = expectedException; + return this; + } + + @Override + public String toString() { + return this.caseName; + } + } +} diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/ArnHandlerTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/ArnHandlerTest.java deleted file mode 100644 index 28a71688b32b..000000000000 --- a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/ArnHandlerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.services.s3control.internal; - - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME; -import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SIGNING_REGION; -import static software.amazon.awssdk.services.s3control.internal.HandlerUtils.X_AMZ_ACCOUNT_ID; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import software.amazon.awssdk.arns.Arn; -import software.amazon.awssdk.core.Protocol; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3control.S3ControlClient; -import software.amazon.awssdk.services.s3control.S3ControlConfiguration; - -public class ArnHandlerTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private SdkHttpRequest request; - private S3ControlConfiguration configuration; - private ExecutionAttributes executionAttributes; - - private final ArnHandler arnHandler = ArnHandler.getInstance(); - private static final String ACCOUNT_ID = "123456789012"; - - @Before - public void setup() { - request = SdkHttpFullRequest.builder() - .appendHeader(X_AMZ_ACCOUNT_ID, ACCOUNT_ID) - .protocol(Protocol.HTTPS.toString()) - .method(SdkHttpMethod.POST) - .host(S3ControlClient.serviceMetadata().endpointFor(Region.US_WEST_2).toString()) - .build(); - configuration = S3ControlConfiguration.builder().build(); - executionAttributes = new ExecutionAttributes(); - executionAttributes.putAttribute(SERVICE_SIGNING_NAME, "s3-control"); - executionAttributes.putAttribute(SIGNING_REGION, Region.of("us-west-2")); - } - - @Test - public void outpostBucketArn_shouldResolveHost() { - Arn arn = Arn.fromString("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"); - SdkHttpRequest modifiedRequest = arnHandler.resolveHostForArn(request, configuration, arn, executionAttributes); - - assertThat(modifiedRequest.host(), is("s3-outposts.us-west-2.amazonaws.com")); - assertThat(executionAttributes.getAttribute(SERVICE_SIGNING_NAME), is("s3-outposts")); - assertThat(modifiedRequest.headers().get("x-amz-outpost-id").get(0), is("op-01234567890123456")); - assertThat(modifiedRequest.headers().get("x-amz-account-id").get(0), is(ACCOUNT_ID)); - } - - @Test - public void outpostAccessPointArn_shouldResolveHost() { - Arn arn = Arn.fromString("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"); - SdkHttpRequest modifiedRequest = arnHandler.resolveHostForArn(request, configuration, arn, executionAttributes); - - assertThat(modifiedRequest.host(), is("s3-outposts.us-west-2.amazonaws.com")); - assertThat(executionAttributes.getAttribute(SERVICE_SIGNING_NAME), is("s3-outposts")); - assertThat(modifiedRequest.headers().get("x-amz-outpost-id").get(0), is("op-01234567890123456")); - assertThat(modifiedRequest.headers().get("x-amz-account-id").get(0), is(ACCOUNT_ID)); - } - - @Test - public void outpostArnWithFipsEnabled_shouldThrowException() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("FIPS"); - - Arn arn = Arn.fromString("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"); - arnHandler.resolveHostForArn(request, enableFips(), arn, executionAttributes); - } - - @Test - public void outpostArnWithDualstackEnabled_shouldThrowException() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Dualstack"); - - Arn arn = Arn.fromString("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"); - arnHandler.resolveHostForArn(request, enableDualstack(), arn, executionAttributes); - } - - private S3ControlConfiguration enableDualstack() { - return S3ControlConfiguration.builder() - .dualstackEnabled(true) - .build(); - } - - private S3ControlConfiguration enableFips() { - return S3ControlConfiguration.builder() - .fipsModeEnabled(true) - .build(); - } -} diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3AccessPointArnTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3AccessPointArnTest.java index b22ede8fec84..feecaf13f5a8 100644 --- a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3AccessPointArnTest.java +++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3AccessPointArnTest.java @@ -60,20 +60,7 @@ public void malformedArn_MissingAccessPointName_shouldThrowException() { exception.expectMessage("Invalid format"); s3Control.getAccessPoint(b -> b.name(accessPointArn)); } - - @Test - public void accessPointArn_ClientHasCustomEndpoint_throwsIllegalArgumentException() { - S3ControlClient s3ControlCustom = buildClientWithCustomEndpoint("https://foo.bar", "us-east-1"); - - String accessPointArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint" - + ":myaccesspoint"; - - exception.expect(IllegalArgumentException.class); - exception.expectMessage("endpoint"); - s3ControlCustom.getAccessPoint(b -> b.name(accessPointArn)); - } - - @Test + public void bucketArnDifferentRegionNoConfigFlag_throwsIllegalArgumentException() { S3ControlClient s3ControlCustom = initializedBuilder().region(Region.of("us-east-1")).build(); String accessPointArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint" diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostAccessPointArnTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostAccessPointArnTest.java index aba2a8b49b23..e18faf9e5584 100644 --- a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostAccessPointArnTest.java +++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostAccessPointArnTest.java @@ -68,14 +68,13 @@ public void regionWithFipsProvided_shouldThrowException() { s3ControlForTest.getAccessPoint(b -> b.name(outpostArn)); } - @Test public void dualstackEnabled_shouldThrowException() { S3ControlClient s3ControlForTest = buildClientCustom().serviceConfiguration(b -> b.dualstackEnabled(true)).build(); String outpostArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"; exception.expect(IllegalArgumentException.class); - exception.expectMessage("Dualstack"); + exception.expectMessage("Dual stack"); s3ControlForTest.getAccessPoint(b -> b.name(outpostArn)); } @@ -112,17 +111,6 @@ public void malformedArn_MissingAccessPointName_shouldThrowException() { s3ControlForTest.getAccessPoint(b -> b.name(outpostArn)); } - @Test - public void outpostArnClientHasCustomEndpoint_throwsIllegalArgumentException() { - S3ControlClient s3Control = buildClientWithCustomEndpoint("https://foo.bar", "us-east-1"); - - String outpostArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"; - - exception.expect(IllegalArgumentException.class); - exception.expectMessage("endpoint"); - s3Control.getAccessPoint(b -> b.name(outpostArn)); - } - @Test public void bucketArnDifferentRegionNoConfigFlag_throwsIllegalArgumentException() { S3ControlClient s3ControlForTest = initializedBuilder().region(Region.of("us-west-2")).build(); diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostBucketArnTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostBucketArnTest.java index b4c308915157..a3d8cb20154b 100644 --- a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostBucketArnTest.java +++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/functionaltests/arns/S3OutpostBucketArnTest.java @@ -72,17 +72,6 @@ public void fipsRegionProvided_shouldThrowException() { s3ControlForTest.getBucket(b -> b.bucket(bucketArn)); } - @Test - public void dualstackEnabled_shouldThrowException() { - S3ControlClient s3ControlForTest = buildClientCustom().serviceConfiguration(b -> b.dualstackEnabled(true)).build(); - - String bucketArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"; - - exception.expect(IllegalArgumentException.class); - exception.expectMessage("Dualstack"); - s3ControlForTest.getBucket(b -> b.bucket(bucketArn)); - } - @Test public void malformedArn_MissingBucketSegment_shouldThrowException() { S3ControlClient s3ControlForTest = buildClientCustom().serviceConfiguration(b -> b.dualstackEnabled(true)).build(); @@ -148,16 +137,6 @@ public void bucketArnInvalidPartition_throwsIllegalArgumentException() { s3ControlForTest.getBucket(b -> b.bucket(bucketArn)); } - @Test - public void bucketArnWithCustomEndpoint_throwsIllegalArgumentException() { - S3ControlClient s3ControlForTest = buildClientWithCustomEndpoint("https://foo.bar", "us-west-2"); - String bucketArn = "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"; - - exception.expect(IllegalArgumentException.class); - exception.expectMessage("has been configured with an endpoint override"); - s3ControlForTest.getBucket(b -> b.bucket(bucketArn)); - } - @Test public void bucketArn_conflictingAccountIdPresent_shouldThrowException() { S3ControlClient s3ControlForTest = initializedBuilder().region(Region.of("us-west-2")).build(); diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptorTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptorTest.java index a909573972d7..ce035d11b194 100644 --- a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptorTest.java +++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/EndpointAddressInterceptorTest.java @@ -16,21 +16,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.verifyZeroInteractions; import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME; import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SIGNING_REGION; +import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.SERVICE_CONFIG; +import static software.amazon.awssdk.services.s3control.internal.S3ControlInternalExecutionAttribute.S3_ARNABLE_FIELD; -import java.net.URI; import java.util.Optional; -import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.arns.Arn; import software.amazon.awssdk.core.Protocol; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.async.AsyncRequestBody; -import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.core.sync.RequestBody; @@ -38,18 +37,22 @@ import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.internal.presigner.DefaultS3Presigner; import software.amazon.awssdk.services.s3control.S3ControlClient; import software.amazon.awssdk.services.s3control.S3ControlConfiguration; +import software.amazon.awssdk.services.s3control.internal.S3ArnableField; import software.amazon.awssdk.services.s3control.model.CreateBucketRequest; import software.amazon.awssdk.services.s3control.model.ListRegionalBucketsRequest; public class EndpointAddressInterceptorTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); private static final String X_AMZ_ACCOUNT_ID = "x-amz-account-id"; private static final String ACCOUNT_ID = "123456789012"; private SdkHttpRequest request; + private S3ControlConfiguration configuration; + private ExecutionAttributes executionAttributes; @Before public void setup() { @@ -59,6 +62,11 @@ public void setup() { .method(SdkHttpMethod.POST) .host(S3ControlClient.serviceMetadata().endpointFor(Region.US_EAST_1).toString()) .build(); + configuration = S3ControlConfiguration.builder().build(); + executionAttributes = new ExecutionAttributes(); + executionAttributes.putAttribute(SERVICE_SIGNING_NAME, "s3-control"); + executionAttributes.putAttribute(SIGNING_REGION, Region.of("us-east-1")); + executionAttributes.putAttribute(SERVICE_CONFIG, configuration); } @Test @@ -196,11 +204,11 @@ public void listRegionalBucketsRequestWithOutpostId_fipsDualsackEnabled_shouldTh executionAttributes.putAttribute(SERVICE_SIGNING_NAME, "s3"); assertThatThrownBy(() -> interceptor.modifyHttpRequest(new Context(request).request(sdkRequest), - executionAttributes)).hasMessageContaining("Dualstack endpoints are " - + "not supported"); + executionAttributes)) + .hasMessageContaining("Dual stack"); } - @Test(expected = SdkClientException.class) + @Test(expected = IllegalArgumentException.class) public void modifyHttpRequest_ThrowsException_FipsAndDualstack() { EndpointAddressInterceptor interceptor = new EndpointAddressInterceptor(); @@ -214,24 +222,74 @@ public void modifyHttpRequest_ThrowsException_FipsAndDualstack() { interceptor.modifyHttpRequest(new Context(request), executionAttributes); } - @Test(expected = SdkClientException.class) - public void modifyHttpRequest_ThrowsException_NonStandardEndpoint() { + @Test + public void outpostBucketArn_shouldResolveHost() { EndpointAddressInterceptor interceptor = new EndpointAddressInterceptor(); - S3ControlConfiguration controlConfiguration = S3ControlConfiguration.builder() - .dualstackEnabled(true) - .build(); - ExecutionAttributes executionAttributes = new ExecutionAttributes(); - executionAttributes.putAttribute(SdkExecutionAttribute.SERVICE_CONFIG, controlConfiguration); + Arn arn = Arn.fromString("arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket"); + executionAttributes.putAttribute(S3_ARNABLE_FIELD, S3ArnableField.builder().arn(arn).build()); + SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(new Context(request), executionAttributes); + + assertThat(modifiedRequest.host()).isEqualTo("s3-outposts.us-east-1.amazonaws.com"); + assertThat(executionAttributes.getAttribute(SERVICE_SIGNING_NAME)).isEqualTo("s3-outposts"); + assertThat(modifiedRequest.headers().get("x-amz-outpost-id").get(0)).isEqualTo("op-01234567890123456"); + assertThat(modifiedRequest.headers().get("x-amz-account-id").get(0)).isEqualTo(ACCOUNT_ID); + } + + @Test + public void outpostAccessPointArn_shouldResolveHost() { + EndpointAddressInterceptor interceptor = new EndpointAddressInterceptor(); + + Arn arn = Arn.fromString("arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"); + executionAttributes.putAttribute(S3_ARNABLE_FIELD, S3ArnableField.builder().arn(arn).build()); + SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(new Context(request), executionAttributes); + + assertThat(modifiedRequest.host()).isEqualTo("s3-outposts.us-east-1.amazonaws.com"); + assertThat(executionAttributes.getAttribute(SERVICE_SIGNING_NAME)).isEqualTo("s3-outposts"); + assertThat(modifiedRequest.headers().get("x-amz-outpost-id").get(0)).isEqualTo("op-01234567890123456"); + assertThat(modifiedRequest.headers().get("x-amz-account-id").get(0)).isEqualTo(ACCOUNT_ID); + } + + @Test + public void outpostArnWithFipsEnabled_shouldThrowException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("FIPS"); + + EndpointAddressInterceptor interceptor = new EndpointAddressInterceptor(); + Arn arn = Arn.fromString("arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket"); + executionAttributes.putAttribute(S3_ARNABLE_FIELD, S3ArnableField.builder().arn(arn).build()); + executionAttributes.putAttribute(SERVICE_CONFIG, enableFips()); + interceptor.modifyHttpRequest(new Context(request), executionAttributes); + } + + @Test + public void outpostArnWithDualstackEnabled_shouldThrowException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Dual stack"); + + EndpointAddressInterceptor interceptor = new EndpointAddressInterceptor(); + Arn arn = Arn.fromString("arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket"); + executionAttributes.putAttribute(S3_ARNABLE_FIELD, S3ArnableField.builder().arn(arn).build()); + executionAttributes.putAttribute(SERVICE_CONFIG, enableDualstack()); + interceptor.modifyHttpRequest(new Context(request), executionAttributes); + } + + private S3ControlConfiguration enableDualstack() { + return S3ControlConfiguration.builder() + .dualstackEnabled(true) + .build(); + } - interceptor.modifyHttpRequest(new Context(request.toBuilder().host("some-garbage").build()), - executionAttributes); + private S3ControlConfiguration enableFips() { + return S3ControlConfiguration.builder() + .fipsModeEnabled(true) + .build(); } public final class Context implements software.amazon.awssdk.core.interceptor.Context.ModifyHttpRequest { private final SdkHttpRequest request; - private SdkRequest sdkRequest; + private SdkRequest sdkRequest = CreateBucketRequest.builder().build(); public Context(SdkHttpRequest request) { this.request = request;