• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 package software.amazon.awssdk.services.s3control;
16 
17 import static org.assertj.core.api.Assertions.assertThat;
18 import static org.junit.Assert.assertEquals;
19 import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
20 import static software.amazon.awssdk.utils.StringUtils.isEmpty;
21 
22 import java.io.IOException;
23 import java.nio.charset.StandardCharsets;
24 import java.time.Duration;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.stream.Stream;
28 import org.junit.jupiter.api.BeforeAll;
29 import org.junit.jupiter.api.Test;
30 import org.junit.jupiter.params.ParameterizedTest;
31 import org.junit.jupiter.params.provider.Arguments;
32 import org.junit.jupiter.params.provider.MethodSource;
33 import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
34 import software.amazon.awssdk.auth.signer.internal.SignerConstant;
35 import software.amazon.awssdk.awscore.presigner.PresignedRequest;
36 import software.amazon.awssdk.core.SdkPlugin;
37 import software.amazon.awssdk.core.interceptor.Context;
38 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
39 import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
40 import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy;
41 import software.amazon.awssdk.core.sync.RequestBody;
42 import software.amazon.awssdk.core.waiters.Waiter;
43 import software.amazon.awssdk.core.waiters.WaiterAcceptor;
44 import software.amazon.awssdk.http.HttpExecuteRequest;
45 import software.amazon.awssdk.http.HttpExecuteResponse;
46 import software.amazon.awssdk.http.SdkHttpRequest;
47 import software.amazon.awssdk.http.apache.ApacheHttpClient;
48 import software.amazon.awssdk.regions.Region;
49 import software.amazon.awssdk.services.s3.S3Client;
50 import software.amazon.awssdk.services.s3.S3Configuration;
51 import software.amazon.awssdk.services.s3.internal.plugins.S3OverrideAuthSchemePropertiesPlugin;
52 import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException;
53 import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
54 import software.amazon.awssdk.services.s3.presigner.S3Presigner;
55 import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
56 import software.amazon.awssdk.services.s3control.model.BucketAlreadyExistsException;
57 import software.amazon.awssdk.services.s3control.model.CreateMultiRegionAccessPointInput;
58 import software.amazon.awssdk.services.s3control.model.GetMultiRegionAccessPointResponse;
59 import software.amazon.awssdk.services.s3control.model.ListMultiRegionAccessPointsResponse;
60 import software.amazon.awssdk.services.s3control.model.MultiRegionAccessPointStatus;
61 import software.amazon.awssdk.services.sts.StsClient;
62 import software.amazon.awssdk.utils.IoUtils;
63 import software.amazon.awssdk.utils.Logger;
64 import software.amazon.awssdk.utils.StringInputStream;
65 
66 public class S3MrapIntegrationTest extends S3ControlIntegrationTestBase {
67     private static final Logger log = Logger.loggerFor(S3MrapIntegrationTest.class);
68 
69     private static final String SIGV4A_CHUNKED_PAYLOAD_SIGNING = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD";
70     private static final String SIGV4_CHUNKED_PAYLOAD_SIGNING = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
71     private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
72     private static final Region REGION = Region.US_WEST_2;
73     private static String bucket;
74     private static String mrapName;
75     private static final String KEY = "aws-java-sdk-small-test-object";
76     private static final String CONTENT = "A short string for a small test object";
77     private static final int RETRY_TIMES = 10;
78     private static final int RETRY_DELAY_IN_SECONDS = 30;
79 
80     private static S3ControlClient s3control;
81     private static CaptureRequestInterceptor captureInterceptor;
82     private static String mrapAlias;
83     private static StsClient stsClient;
84     private static S3Client s3Client;
85     private static S3Client s3ClientWithPayloadSigning;
86 
87     @BeforeAll
setupFixture()88     public static void setupFixture() {
89         captureInterceptor = new CaptureRequestInterceptor();
90 
91         s3control = S3ControlClient.builder()
92                                    .region(REGION)
93                                    .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
94                                    .build();
95 
96         s3Client = mrapEnabledS3Client(Collections.singletonList(captureInterceptor));
97         s3ClientWithPayloadSigning = mrapEnabledS3ClientWithPayloadSigning(captureInterceptor);
98 
99         stsClient = StsClient.builder()
100                              .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
101                              .region(REGION)
102                              .build();
103         accountId = stsClient.getCallerIdentity().account();
104         bucket = "do-not-delete-s3mraptest-" + accountId;
105         mrapName = "javaintegtest" + accountId;
106         log.info(() -> "bucket " + bucket);
107 
108         createBucketIfNotExist(bucket);
109         createMrapIfNotExist(accountId, mrapName);
110         mrapAlias = getMrapAliasAndVerify(accountId, mrapName);
111     }
112 
113     @ParameterizedTest(name = "{index}:key = {1},       {0}")
114     @MethodSource("keys")
when_callingMrapWithDifferentPaths_unsignedPayload_requestIsAccepted(String name, String key, String expected)115     public void when_callingMrapWithDifferentPaths_unsignedPayload_requestIsAccepted(String name, String key, String expected) {
116         putGetDeleteObjectMrap(s3Client, UNSIGNED_PAYLOAD, key, expected);
117     }
118 
119     @ParameterizedTest(name = "{index}:key = {1},       {0}")
120     @MethodSource("keys")
when_callingMrapWithDifferentPaths_signedPayload_requestIsAccepted(String name, String key, String expected)121     public void when_callingMrapWithDifferentPaths_signedPayload_requestIsAccepted(String name, String key, String expected) {
122         putGetDeleteObjectMrap(s3ClientWithPayloadSigning, SIGV4A_CHUNKED_PAYLOAD_SIGNING, key, expected);
123     }
124 
125     @ParameterizedTest(name = "{index}:key = {1},       {0}")
126     @MethodSource("keys")
when_callingS3WithDifferentPaths_unsignedPayload_requestIsAccepted(String name, String key, String expected)127     public void when_callingS3WithDifferentPaths_unsignedPayload_requestIsAccepted(String name, String key, String expected) {
128         putGetDeleteObjectStandard(s3Client, UNSIGNED_PAYLOAD, key, expected);
129     }
130 
131     @ParameterizedTest(name = "{index}:key = {1},       {0}")
132     @MethodSource("keys")
when_callingS3WithDifferentPaths_signedPayload_requestIsAccepted(String name, String key, String expected)133     public void when_callingS3WithDifferentPaths_signedPayload_requestIsAccepted(String name, String key, String expected) {
134         putGetDeleteObjectStandard(s3ClientWithPayloadSigning, SIGV4_CHUNKED_PAYLOAD_SIGNING, key, expected);
135     }
136 
137     @Test
when_creatingPresignedMrapUrl_getRequestWorks()138     public void when_creatingPresignedMrapUrl_getRequestWorks() {
139         S3Presigner presigner = s3Presigner();
140         PresignedGetObjectRequest presignedGetObjectRequest =
141             presigner.presignGetObject(p -> p.getObjectRequest(r -> r.bucket(constructMrapArn(accountId, mrapAlias)).key(KEY))
142                                              .signatureDuration(Duration.ofMinutes(10)));
143 
144         deleteObjectIfExists(s3Client, constructMrapArn(accountId, mrapAlias), KEY);
145         s3Client.putObject(r -> r.bucket(constructMrapArn(accountId, mrapAlias)).key(KEY), RequestBody.fromString(CONTENT));
146 
147         String object = applyPresignedUrl(presignedGetObjectRequest, null);
148         assertEquals(CONTENT, object);
149         verifySigv4aRequest(captureInterceptor.request(), UNSIGNED_PAYLOAD);
150     }
151 
putGetDeleteObjectMrap(S3Client testClient, String payloadSigningTag, String key, String expected)152     public void putGetDeleteObjectMrap(S3Client testClient, String payloadSigningTag, String key, String expected) {
153         deleteObjectIfExists(testClient, constructMrapArn(accountId, mrapAlias), key);
154         testClient.putObject(r -> r.bucket(constructMrapArn(accountId, mrapAlias)).key(key), RequestBody.fromString(CONTENT));
155         verifySigv4aRequest(captureInterceptor.request(), payloadSigningTag);
156 
157         String object = testClient.getObjectAsBytes(r -> r.bucket(constructMrapArn(accountId, mrapAlias)).key(key)).asString(StandardCharsets.UTF_8);
158         assertEquals(CONTENT, object);
159         verifySigv4aRequest(captureInterceptor.request(), UNSIGNED_PAYLOAD);
160 
161         testClient.deleteObject(r -> r.bucket(constructMrapArn(accountId, mrapAlias)).key(key));
162         verifySigv4aRequest(captureInterceptor.request(), UNSIGNED_PAYLOAD);
163 
164         assertThat(captureInterceptor.normalizePath).isNotNull().isEqualTo(false);
165         assertThat(captureInterceptor.request.encodedPath()).isEqualTo(expected);
166     }
167 
putGetDeleteObjectStandard(S3Client testClient, String payloadSigningTag, String key, String expected)168     public void putGetDeleteObjectStandard(S3Client testClient, String payloadSigningTag, String key, String expected) {
169         deleteObjectIfExists(testClient, bucket, key);
170         testClient.putObject(r -> r.bucket(bucket).key(key), RequestBody.fromString(CONTENT));
171         verifySigv4Request(captureInterceptor.request(), payloadSigningTag);
172 
173         String object = testClient.getObjectAsBytes(r -> r.bucket(bucket).key(key)).asString(StandardCharsets.UTF_8);
174         assertEquals(CONTENT, object);
175         verifySigv4Request(captureInterceptor.request(), UNSIGNED_PAYLOAD);
176 
177         testClient.deleteObject(r -> r.bucket(bucket).key(key));
178         verifySigv4Request(captureInterceptor.request(), UNSIGNED_PAYLOAD);
179 
180         assertThat(captureInterceptor.normalizePath).isNotNull().isEqualTo(false);
181         assertThat(captureInterceptor.request.encodedPath()).isEqualTo(expected);
182     }
183 
verifySigv4aRequest(SdkHttpRequest signedRequest, String payloadSigningTag)184     private void verifySigv4aRequest(SdkHttpRequest signedRequest, String payloadSigningTag) {
185         assertThat(signedRequest.headers().get("Authorization").get(0)).contains("AWS4-ECDSA-P256-SHA256");
186         assertThat(signedRequest.headers().get("Host").get(0)).isEqualTo(constructMrapHostname(mrapAlias));
187         assertThat(signedRequest.headers().get("x-amz-content-sha256").get(0)).isEqualTo(payloadSigningTag);
188         assertThat(signedRequest.headers().get("X-Amz-Date").get(0)).isNotEmpty();
189         assertThat(signedRequest.headers().get("X-Amz-Region-Set").get(0)).isEqualTo("*");
190     }
191 
verifySigv4Request(SdkHttpRequest signedRequest, String payloadSigningTag)192     private void verifySigv4Request(SdkHttpRequest signedRequest, String payloadSigningTag) {
193         assertThat(signedRequest.headers().get("Authorization").get(0)).contains(SignerConstant.AWS4_SIGNING_ALGORITHM);
194         assertThat(signedRequest.headers().get("Host").get(0)).isEqualTo(String.format("%s.s3.%s.amazonaws.com",
195                                                                                        bucket, REGION.id()));
196         assertThat(signedRequest.headers().get("x-amz-content-sha256").get(0)).isEqualTo(payloadSigningTag);
197         assertThat(signedRequest.headers().get("X-Amz-Date").get(0)).isNotEmpty();
198     }
199 
keys()200     private static Stream<Arguments> keys() {
201         return Stream.of(
202             Arguments.of("Slash -> unchanged", "/", "//"),
203             Arguments.of("Single segment with initial slash -> unchanged", "/foo", "//foo"),
204             Arguments.of("Single segment no slash -> slash prepended", "foo", "/foo"),
205             Arguments.of("Multiple segments -> unchanged", "/foo/bar", "//foo/bar"),
206             Arguments.of("Multiple segments with trailing slash -> unchanged", "/foo/bar/", "//foo/bar/"),
207             Arguments.of("Multiple segments, urlEncoded slash -> encodes percent", "/foo%2Fbar", "//foo%252Fbar"),
208             Arguments.of("Single segment, dot -> should remove dot", "/.", "//."),
209             Arguments.of("Multiple segments with dot -> should remove dot", "/foo/./bar", "//foo/./bar"),
210             Arguments.of("Multiple segments with ending dot -> should remove dot and trailing slash", "/foo/bar/.", "//foo/bar/."),
211             Arguments.of("Multiple segments with dots -> should remove dots and preceding segment", "/foo/bar/../baz", "//foo/bar/../baz"),
212             Arguments.of("First segment has colon -> unchanged, url encoded first", "foo:/bar", "/foo%3A/bar"),
213             Arguments.of("Multiple segments, urlEncoded slash -> encodes percent", "/foo%2F.%2Fbar", "//foo%252F.%252Fbar"),
214             Arguments.of("No url encode, Multiple segments with dot -> unchanged", "/foo/./bar", "//foo/./bar"),
215             Arguments.of("Multiple segments with dots -> unchanged", "/foo/bar/../baz", "//foo/bar/../baz"),
216             Arguments.of("double slash", "//H", "///H"),
217             Arguments.of("double slash in middle", "A//H", "/A//H")
218         );
219     }
220 
constructMrapArn(String account, String mrapAlias)221     private String constructMrapArn(String account, String mrapAlias) {
222         return String.format("arn:aws:s3::%s:accesspoint:%s", account, mrapAlias);
223     }
224 
constructMrapHostname(String mrapAlias)225     private String constructMrapHostname(String mrapAlias) {
226         return String.format("%s.accesspoint.s3-global.amazonaws.com", mrapAlias);
227     }
228 
s3Presigner()229     private S3Presigner s3Presigner() {
230         return S3Presigner.builder()
231                           .region(REGION)
232                           .serviceConfiguration(S3Configuration.builder()
233                                                                .useArnRegionEnabled(true)
234                                                                .checksumValidationEnabled(false)
235                                                                .build())
236                           .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
237                           .build();
238     }
239 
createMrapIfNotExist(String account, String mrapName)240     private static void createMrapIfNotExist(String account, String mrapName) {
241         software.amazon.awssdk.services.s3control.model.Region mrapRegion =
242             software.amazon.awssdk.services.s3control.model.Region.builder().bucket(bucket).build();
243 
244         boolean mrapNotExists = s3control.listMultiRegionAccessPoints(r -> r.accountId(account))
245                                          .accessPoints().stream()
246                                          .noneMatch(a -> a.name().equals(S3MrapIntegrationTest.mrapName));
247         if (mrapNotExists) {
248             CreateMultiRegionAccessPointInput details = CreateMultiRegionAccessPointInput.builder()
249                                                                                          .name(mrapName)
250                                                                                          .regions(mrapRegion)
251                                                                                          .build();
252             log.info(() -> "Creating MRAP: " + mrapName);
253             s3control.createMultiRegionAccessPoint(r -> r.accountId(account).details(details));
254             waitForResourceCreation(mrapName);
255         }
256     }
257 
waitForResourceCreation(String mrapName)258     private static void waitForResourceCreation(String mrapName) throws IllegalStateException {
259         Waiter<ListMultiRegionAccessPointsResponse> waiter =
260             Waiter.builder(ListMultiRegionAccessPointsResponse.class)
261                   .addAcceptor(WaiterAcceptor.successOnResponseAcceptor(r ->
262                       r.accessPoints().stream().findFirst().filter(mrap -> mrap.name().equals(mrapName) && mrap.status().equals(MultiRegionAccessPointStatus.READY)).isPresent()
263                   ))
264                   .addAcceptor(WaiterAcceptor.retryOnResponseAcceptor(i -> true))
265                 .overrideConfiguration(b -> b.maxAttempts(RETRY_TIMES).backoffStrategy(FixedDelayBackoffStrategy.create(Duration.ofSeconds(RETRY_DELAY_IN_SECONDS))))
266                 .build();
267 
268         waiter.run(() -> s3control.listMultiRegionAccessPoints(r -> r.accountId(accountId)));
269     }
270 
getMrapAliasAndVerify(String account, String mrapName)271     public static String getMrapAliasAndVerify(String account, String mrapName) {
272         GetMultiRegionAccessPointResponse mrap = s3control.getMultiRegionAccessPoint(r -> r.accountId(account).name(mrapName));
273         assertThat(mrap.accessPoint()).isNotNull();
274         assertThat(mrap.accessPoint().name()).isEqualTo(mrapName);
275         log.info(() -> "Alias: " + mrap.accessPoint().alias());
276         return mrap.accessPoint().alias();
277     }
278 
applyPresignedUrl(PresignedRequest presignedRequest, String content)279     private String applyPresignedUrl(PresignedRequest presignedRequest, String content) {
280         try {
281             HttpExecuteRequest.Builder builder = HttpExecuteRequest.builder().request(presignedRequest.httpRequest());
282             if (!isEmpty(content)) {
283                 builder.contentStreamProvider(() -> new StringInputStream(content));
284             }
285             HttpExecuteRequest request = builder.build();
286             HttpExecuteResponse response = ApacheHttpClient.create().prepareRequest(request).call();
287             return response.responseBody()
288                            .map(stream -> invokeSafely(() -> IoUtils.toUtf8String(stream)))
289                            .orElseThrow(() -> new IOException("No input stream"));
290         } catch (IOException e) {
291             log.error(() -> "Error occurred ", e);
292         }
293         return null;
294     }
295 
mrapEnabledS3Client(List<ExecutionInterceptor> executionInterceptors)296     private static S3Client mrapEnabledS3Client(List<ExecutionInterceptor> executionInterceptors) {
297         return S3Client.builder()
298                        .region(REGION)
299                        .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
300                        .serviceConfiguration(S3Configuration.builder()
301                                                             .useArnRegionEnabled(true)
302                                                             .build())
303                        .overrideConfiguration(o -> o.executionInterceptors(executionInterceptors))
304                        .build();
305     }
306 
mrapEnabledS3ClientWithPayloadSigning(ExecutionInterceptor executionInterceptor)307     private static S3Client mrapEnabledS3ClientWithPayloadSigning(ExecutionInterceptor executionInterceptor) {
308         // We can't use here `S3OverrideAuthSchemePropertiesPlugin.enablePayloadSigningPlugin()` since
309         // it enables payload signing for *all* operations.
310         SdkPlugin plugin = S3OverrideAuthSchemePropertiesPlugin.builder()
311                                                                .payloadSigningEnabled(true)
312                                                                .addOperationConstraint("UploadPart")
313                                                                .addOperationConstraint("PutObject")
314                                                                .build();
315         return S3Client.builder()
316                        .region(REGION)
317                        .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
318                        .serviceConfiguration(S3Configuration.builder()
319                                                             .useArnRegionEnabled(true)
320                                                             .build())
321                        .overrideConfiguration(o -> o.addExecutionInterceptor(executionInterceptor))
322                        .addPlugin(plugin)
323                        .build();
324     }
325 
deleteObjectIfExists(S3Client s31, String bucket1, String key)326     private void deleteObjectIfExists(S3Client s31, String bucket1, String key) {
327         System.out.println(bucket1);
328         try {
329             s31.deleteObject(r -> r.bucket(bucket1).key(key));
330         } catch (NoSuchKeyException e) {
331         }
332     }
333 
createBucketIfNotExist(String bucket)334     private static void createBucketIfNotExist(String bucket) {
335         try {
336             s3Client.createBucket(b -> b.bucket(bucket));
337             s3Client.waiter().waitUntilBucketExists(b -> b.bucket(bucket));
338         } catch (BucketAlreadyOwnedByYouException | BucketAlreadyExistsException e) {
339             // ignore
340         }
341     }
342 
343     private static class CaptureRequestInterceptor implements ExecutionInterceptor {
344 
345         private SdkHttpRequest request;
346         private Boolean normalizePath;
347 
request()348         public SdkHttpRequest request() {
349             return request;
350         }
351 
352         @Override
beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes)353         public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
354             this.request = context.httpRequest();
355             this.normalizePath = executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH);
356         }
357     }
358 }
359