Skip to content

Commit 32fb40f

Browse files
authored
Add additional backwards-compatibility testing for legacy clients. (#4493)
* Add additional backwards-compatibility testing for legacy clients. 1. Ensure anonymous credentials function correctly 2. Ensure attributes set in custom execution interceptors make it to the legacy signer
1 parent b44d0a6 commit 32fb40f

File tree

4 files changed

+364
-1
lines changed

4 files changed

+364
-1
lines changed

core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionAttribute.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,10 @@ public void set(Map<ExecutionAttribute<?>, Object> attributes, T value) {
269269

270270
@Override
271271
public void setIfAbsent(Map<ExecutionAttribute<?>, Object> attributes, T value) {
272-
attributes.computeIfAbsent(realAttribute, k -> writeMapping.apply(null, value));
272+
T currentValue = get(attributes);
273+
if (currentValue == null) {
274+
set(attributes, value);
275+
}
273276
}
274277
}
275278

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
import java.util.Arrays;
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.function.Consumer;
23+
import java.util.function.Function;
24+
import org.junit.jupiter.api.Test;
25+
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
26+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
27+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
28+
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
29+
import software.amazon.awssdk.core.SdkRequest;
30+
import software.amazon.awssdk.core.async.AsyncRequestBody;
31+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
32+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
33+
import software.amazon.awssdk.core.interceptor.Context;
34+
import software.amazon.awssdk.core.interceptor.ExecutionAttribute;
35+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
36+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
37+
import software.amazon.awssdk.core.signer.Signer;
38+
import software.amazon.awssdk.core.sync.RequestBody;
39+
import software.amazon.awssdk.http.HttpExecuteResponse;
40+
import software.amazon.awssdk.http.SdkHttpFullRequest;
41+
import software.amazon.awssdk.http.SdkHttpRequest;
42+
import software.amazon.awssdk.http.SdkHttpResponse;
43+
import software.amazon.awssdk.http.auth.aws.scheme.AwsV4AuthScheme;
44+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
45+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
46+
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
47+
import software.amazon.awssdk.http.auth.spi.signer.AsyncSignRequest;
48+
import software.amazon.awssdk.http.auth.spi.signer.AsyncSignedRequest;
49+
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
50+
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
51+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
52+
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
53+
import software.amazon.awssdk.identity.spi.IdentityProvider;
54+
import software.amazon.awssdk.identity.spi.IdentityProviders;
55+
import software.amazon.awssdk.regions.Region;
56+
import software.amazon.awssdk.services.s3.S3AsyncClient;
57+
import software.amazon.awssdk.services.s3.S3AsyncClientBuilder;
58+
import software.amazon.awssdk.services.s3.S3Client;
59+
import software.amazon.awssdk.services.s3.S3ClientBuilder;
60+
import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm;
61+
import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient;
62+
import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient;
63+
64+
/**
65+
* Ensure that attributes set in execution interceptors are passed to custom signers. These are protected APIs, but code
66+
* searches show that customers are using them as if they aren't. We should push customers onto supported paths.
67+
*/
68+
public class InterceptorSignerAttributeTest {
69+
private static final AwsCredentials CREDS = AwsBasicCredentials.create("akid", "skid");
70+
71+
@Test
72+
public void canSetSignerExecutionAttributes_beforeExecution() {
73+
test(attributeModifications -> new ExecutionInterceptor() {
74+
@Override
75+
public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) {
76+
attributeModifications.accept(executionAttributes);
77+
}
78+
},
79+
AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, // Endpoint rules override signing name
80+
AwsSignerExecutionAttribute.SIGNING_REGION, // Endpoint rules override signing region
81+
AwsSignerExecutionAttribute.AWS_CREDENTIALS, // Legacy auth strategy overrides credentials
82+
AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE); // Endpoint rules override double-url-encode
83+
}
84+
85+
@Test
86+
public void canSetSignerExecutionAttributes_modifyRequest() {
87+
test(attributeModifications -> new ExecutionInterceptor() {
88+
@Override
89+
public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
90+
attributeModifications.accept(executionAttributes);
91+
return context.request();
92+
}
93+
},
94+
AwsSignerExecutionAttribute.AWS_CREDENTIALS); // Legacy auth strategy overrides credentials
95+
}
96+
97+
@Test
98+
public void canSetSignerExecutionAttributes_beforeMarshalling() {
99+
test(attributeModifications -> new ExecutionInterceptor() {
100+
@Override
101+
public void beforeMarshalling(Context.BeforeMarshalling context, ExecutionAttributes executionAttributes) {
102+
attributeModifications.accept(executionAttributes);
103+
}
104+
});
105+
}
106+
107+
@Test
108+
public void canSetSignerExecutionAttributes_afterMarshalling() {
109+
test(attributeModifications -> new ExecutionInterceptor() {
110+
@Override
111+
public void afterMarshalling(Context.AfterMarshalling context, ExecutionAttributes executionAttributes) {
112+
attributeModifications.accept(executionAttributes);
113+
}
114+
});
115+
}
116+
117+
@Test
118+
public void canSetSignerExecutionAttributes_modifyHttpRequest() {
119+
test(attributeModifications -> new ExecutionInterceptor() {
120+
@Override
121+
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
122+
attributeModifications.accept(executionAttributes);
123+
return context.httpRequest();
124+
}
125+
});
126+
}
127+
128+
private void test(Function<Consumer<ExecutionAttributes>, ExecutionInterceptor> interceptorFactory,
129+
ExecutionAttribute<?>... attributesToExcludeFromTest) {
130+
Set<ExecutionAttribute<?>> attributesToExclude = new HashSet<>(Arrays.asList(attributesToExcludeFromTest));
131+
132+
ExecutionInterceptor interceptor = interceptorFactory.apply(executionAttributes -> {
133+
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "signing-name");
134+
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, Region.of("signing-region"));
135+
executionAttributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CREDS);
136+
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE, true);
137+
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH, true);
138+
});
139+
140+
ClientOverrideConfiguration.Builder configBuilder =
141+
ClientOverrideConfiguration.builder()
142+
.addExecutionInterceptor(interceptor);
143+
144+
try (MockSyncHttpClient httpClient = new MockSyncHttpClient();
145+
MockAsyncHttpClient asyncHttpClient = new MockAsyncHttpClient()) {
146+
stub200Responses(httpClient, asyncHttpClient);
147+
148+
S3ClientBuilder s3Builder = createS3Builder(configBuilder, httpClient);
149+
S3AsyncClientBuilder s3AsyncBuilder = createS3AsyncBuilder(configBuilder, asyncHttpClient);
150+
151+
CapturingSigner signer1 = new CapturingSigner();
152+
try (S3Client s3 = s3Builder.overrideConfiguration(configBuilder.putAdvancedOption(SdkAdvancedClientOption.SIGNER,
153+
signer1)
154+
.build())
155+
.build()) {
156+
callS3(s3);
157+
validateLegacySignRequest(attributesToExclude, signer1);
158+
}
159+
160+
CapturingSigner signer2 = new CapturingSigner();
161+
try (S3AsyncClient s3 =
162+
s3AsyncBuilder.overrideConfiguration(configBuilder.putAdvancedOption(SdkAdvancedClientOption.SIGNER, signer2)
163+
.build())
164+
.build()) {
165+
callS3(s3);
166+
validateLegacySignRequest(attributesToExclude, signer2);
167+
}
168+
}
169+
}
170+
171+
private static void stub200Responses(MockSyncHttpClient httpClient, MockAsyncHttpClient asyncHttpClient) {
172+
HttpExecuteResponse response =
173+
HttpExecuteResponse.builder()
174+
.response(SdkHttpResponse.builder()
175+
.statusCode(200)
176+
.build())
177+
.build();
178+
httpClient.stubResponses(response);
179+
asyncHttpClient.stubResponses(response);
180+
}
181+
182+
private static S3ClientBuilder createS3Builder(ClientOverrideConfiguration.Builder configBuilder, MockSyncHttpClient httpClient) {
183+
return S3Client.builder()
184+
.region(Region.US_WEST_2)
185+
.credentialsProvider(AnonymousCredentialsProvider.create())
186+
.httpClient(httpClient)
187+
.overrideConfiguration(configBuilder.build());
188+
}
189+
190+
private static S3AsyncClientBuilder createS3AsyncBuilder(ClientOverrideConfiguration.Builder configBuilder, MockAsyncHttpClient asyncHttpClient) {
191+
return S3AsyncClient.builder()
192+
.region(Region.US_WEST_2)
193+
.credentialsProvider(AnonymousCredentialsProvider.create())
194+
.httpClient(asyncHttpClient)
195+
.overrideConfiguration(configBuilder.build());
196+
}
197+
198+
private static void callS3(S3Client s3) {
199+
s3.putObject(r -> r.bucket("foo")
200+
.key("bar")
201+
.checksumAlgorithm(ChecksumAlgorithm.CRC32),
202+
RequestBody.fromString("text"));
203+
}
204+
205+
private void callS3(S3AsyncClient s3) {
206+
s3.putObject(r -> r.bucket("foo")
207+
.key("bar")
208+
.checksumAlgorithm(ChecksumAlgorithm.CRC32),
209+
AsyncRequestBody.fromString("text"))
210+
.join();
211+
}
212+
213+
private void validateLegacySignRequest(Set<ExecutionAttribute<?>> attributesToExclude, CapturingSigner signer) {
214+
if (!attributesToExclude.contains(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME)) {
215+
assertThat(signer.executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME))
216+
.isEqualTo("signing-name");
217+
}
218+
if (!attributesToExclude.contains(AwsSignerExecutionAttribute.SIGNING_REGION)) {
219+
assertThat(signer.executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
220+
.isEqualTo(Region.of("signing-region"));
221+
}
222+
if (!attributesToExclude.contains(AwsSignerExecutionAttribute.AWS_CREDENTIALS)) {
223+
assertThat(signer.executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS))
224+
.isEqualTo(CREDS);
225+
}
226+
if (!attributesToExclude.contains(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE)) {
227+
assertThat(signer.executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE))
228+
.isEqualTo(true);
229+
}
230+
if (!attributesToExclude.contains(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH)) {
231+
assertThat(signer.executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH))
232+
.isEqualTo(true);
233+
}
234+
}
235+
236+
private static class CapturingSigner implements Signer {
237+
private ExecutionAttributes executionAttributes;
238+
239+
@Override
240+
public SdkHttpFullRequest sign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes) {
241+
this.executionAttributes = executionAttributes.copy();
242+
return request;
243+
}
244+
}
245+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
import org.junit.jupiter.api.AfterEach;
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
22+
import software.amazon.awssdk.core.async.AsyncRequestBody;
23+
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
24+
import software.amazon.awssdk.core.sync.RequestBody;
25+
import software.amazon.awssdk.core.sync.ResponseTransformer;
26+
import software.amazon.awssdk.regions.Region;
27+
import software.amazon.awssdk.services.s3.S3AsyncClient;
28+
import software.amazon.awssdk.services.s3.S3Client;
29+
import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient;
30+
import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient;
31+
32+
/**
33+
* Ensure that we can make anonymous requests using S3.
34+
*/
35+
public class S3AnonymousTest {
36+
private MockSyncHttpClient httpClient;
37+
private MockAsyncHttpClient asyncHttpClient;
38+
private S3Client s3;
39+
private S3AsyncClient s3Async;
40+
41+
@BeforeEach
42+
public void setup() {
43+
this.httpClient = new MockSyncHttpClient();
44+
this.httpClient.stubNextResponse200();
45+
46+
this.asyncHttpClient = new MockAsyncHttpClient();
47+
this.asyncHttpClient.stubNextResponse200();
48+
49+
this.s3 = S3Client.builder()
50+
.region(Region.US_WEST_2)
51+
.credentialsProvider(AnonymousCredentialsProvider.create())
52+
.httpClient(httpClient)
53+
.build();
54+
55+
this.s3Async = S3AsyncClient.builder()
56+
.region(Region.US_WEST_2)
57+
.credentialsProvider(AnonymousCredentialsProvider.create())
58+
.httpClient(asyncHttpClient)
59+
.build();
60+
}
61+
62+
@AfterEach
63+
public void teardown() {
64+
httpClient.close();
65+
asyncHttpClient.close();
66+
s3.close();
67+
s3Async.close();
68+
}
69+
70+
@Test
71+
public void nonStreamingOperations_canBeAnonymous() {
72+
s3.listBuckets();
73+
assertThat(httpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
74+
}
75+
76+
@Test
77+
public void nonStreamingOperations_async_canBeAnonymous() {
78+
s3Async.listBuckets().join();
79+
assertThat(asyncHttpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
80+
}
81+
82+
@Test
83+
public void streamingWriteOperations_canBeAnonymous() {
84+
s3.putObject(r -> r.bucket("bucket").key("key"), RequestBody.fromString("foo"));
85+
assertThat(httpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
86+
}
87+
88+
@Test
89+
public void streamingWriteOperations_async_canBeAnonymous() {
90+
s3Async.putObject(r -> r.bucket("bucket").key("key"), AsyncRequestBody.fromString("foo")).join();
91+
assertThat(asyncHttpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
92+
}
93+
94+
@Test
95+
public void streamingReadOperations_canBeAnonymous() {
96+
s3.getObject(r -> r.bucket("bucket").key("key"), ResponseTransformer.toBytes());
97+
assertThat(httpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
98+
}
99+
100+
@Test
101+
public void streamingReadOperations_async_canBeAnonymous() {
102+
s3Async.getObject(r -> r.bucket("bucket").key("key"), AsyncResponseTransformer.toBytes()).join();
103+
assertThat(asyncHttpClient.getLastRequest().firstMatchingHeader("Authorization")).isEmpty();
104+
}
105+
}

test/service-test-utils/src/main/java/software/amazon/awssdk/testutils/service/http/MockHttpClient.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import software.amazon.awssdk.http.HttpExecuteResponse;
2121
import software.amazon.awssdk.http.SdkHttpRequest;
22+
import software.amazon.awssdk.http.SdkHttpResponse;
2223
import software.amazon.awssdk.utils.Pair;
2324

2425
public interface MockHttpClient {
@@ -32,6 +33,15 @@ public interface MockHttpClient {
3233
*/
3334
void stubNextResponse(HttpExecuteResponse nextResponse);
3435

36+
default void stubNextResponse200() {
37+
stubNextResponse(HttpExecuteResponse.builder()
38+
.response(SdkHttpResponse.builder()
39+
.statusCode(200)
40+
.putHeader("Content-Length", "0")
41+
.build())
42+
.build());
43+
}
44+
3545
/**
3646
* Sets up the next HTTP response that will be returned by the mock with a delay. Removes responses previously added to the
3747
* mock.

0 commit comments

Comments
 (0)