Skip to content
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AmazonS3-bf3403d.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon S3",
"contributor": "",
"description": "Added support of Request-level credentials override in DefaultS3CrtAsyncClient. See [#5354](https://github.com/aws/aws-sdk-java-v2/issues/5354)."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.crt;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;

import java.io.File;
import java.util.concurrent.CompletionException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3IntegrationTestBase;
import software.amazon.awssdk.services.s3.internal.crt.S3CrtAsyncClient;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.testutils.RandomTempFile;
import software.amazon.awssdk.testutils.service.AwsTestBase;

/**
* Integration tests verifying that request-level credential overrides work correctly
* with the S3 CRT client.
*/
public class S3CrtRequestLevelCredentialsIntegrationTest extends S3IntegrationTestBase {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be an integration test? Can it be written to use wiremock?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Changed to use wiremock instead.

private static final String BUCKET = temporaryBucketName(S3CrtRequestLevelCredentialsIntegrationTest.class);
private static final String KEY = "request-level-creds-test-key";
private static File testFile;

private static final StaticCredentialsProvider INVALID_CREDENTIALS =
StaticCredentialsProvider.create(AwsBasicCredentials.create("invalidAccessKey", "invalidSecretKey"));

@BeforeAll
public static void setup() throws Exception {
S3IntegrationTestBase.setUp();
S3IntegrationTestBase.createBucket(BUCKET);
testFile = new RandomTempFile(1024);
S3IntegrationTestBase.s3.putObject(PutObjectRequest.builder()
.bucket(BUCKET)
.key(KEY)
.build(), testFile.toPath());
}

@AfterAll
public static void cleanup() {
S3IntegrationTestBase.deleteBucketAndAllContents(BUCKET);
}

@Test
void getObject_withValidRequestLevelCredentials_overridingInvalidClientCredentials_shouldSucceed() {
// Client is built with INVALID credentials, but the request overrides with VALID ones.
// If request-level override works, the request should succeed.
try (S3AsyncClient crtClient = S3CrtAsyncClient.builder()
.region(DEFAULT_REGION)
.credentialsProvider(INVALID_CREDENTIALS)
.build()) {

byte[] result = crtClient.getObject(
b -> b.bucket(BUCKET).key(KEY)
.overrideConfiguration(o -> o.credentialsProvider(AwsTestBase.CREDENTIALS_PROVIDER_CHAIN)),
AsyncResponseTransformer.toBytes()).join().asByteArray();

assertThat(result).hasSize(1024);
}
}

@Test
void putObject_withValidRequestLevelCredentials_overridingInvalidClientCredentials_shouldSucceed() {
String overrideKey = KEY + "-put-override";
try (S3AsyncClient crtClient = S3CrtAsyncClient.builder()

.region(DEFAULT_REGION)
.credentialsProvider(INVALID_CREDENTIALS)
.build()) {

crtClient.putObject(
b -> b.bucket(BUCKET).key(overrideKey)
.overrideConfiguration(o -> o.credentialsProvider(AwsTestBase.CREDENTIALS_PROVIDER_CHAIN)),
AsyncRequestBody.fromString("hello")).join();

byte[] content = S3IntegrationTestBase.s3.getObjectAsBytes(
b -> b.bucket(BUCKET).key(overrideKey)).asByteArray();
assertThat(new String(content)).isEqualTo("hello");
}
}

@Test
void getObject_withInvalidRequestLevelCredentials_overridingValidClientCredentials_shouldFail() {
// Client is built with VALID credentials, but the request overrides with INVALID ones.
// The request should fail with a signing/auth error, proving the override is actually used.
try (S3AsyncClient crtClient = S3CrtAsyncClient.builder()
.region(DEFAULT_REGION)
.credentialsProvider(AwsTestBase.CREDENTIALS_PROVIDER_CHAIN)
.build()) {

assertThatThrownBy(() -> crtClient.getObject(
b -> b.bucket(BUCKET).key(KEY)
.overrideConfiguration(o -> o.credentialsProvider(INVALID_CREDENTIALS)),
AsyncResponseTransformer.toBytes()).join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(S3Exception.class);
}
}

@Test
void getObject_withValidClientCredentials_noOverride_shouldSucceed() {
// Baseline: client-level credentials work when no override is provided.
try (S3AsyncClient crtClient = S3CrtAsyncClient.builder()
.region(DEFAULT_REGION)
.credentialsProvider(AwsTestBase.CREDENTIALS_PROVIDER_CHAIN)
.build()) {

byte[] result = crtClient.getObject(
b -> b.bucket(BUCKET).key(KEY),
AsyncResponseTransformer.toBytes()).join().asByteArray();

assertThat(result).hasSize(1024);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -423,24 +423,37 @@
existingHttpAttributes.toBuilder() :
SdkHttpExecutionAttributes.builder();

SdkHttpExecutionAttributes attributes =
builder.put(OPERATION_NAME,
executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME))
.put(HTTP_CHECKSUM, executionAttributes.getAttribute(SdkInternalExecutionAttribute.HTTP_CHECKSUM))
.put(SIGNING_REGION, executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
.put(S3InternalSdkHttpExecutionAttribute.OBJECT_FILE_PATH,
executionAttributes.getAttribute(OBJECT_FILE_PATH))
.put(USE_S3_EXPRESS_AUTH, S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes))
.put(SIGNING_NAME, executionAttributes.getAttribute(SERVICE_SIGNING_NAME))
.put(REQUEST_CHECKSUM_CALCULATION,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.REQUEST_CHECKSUM_CALCULATION))
.put(RESPONSE_CHECKSUM_VALIDATION,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION))
.put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_PATH,
executionAttributes.getAttribute(RESPONSE_FILE_PATH))
.put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_OPTION,
executionAttributes.getAttribute(RESPONSE_FILE_OPTION))
.build();
builder.put(OPERATION_NAME,
executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME))
.put(HTTP_CHECKSUM, executionAttributes.getAttribute(SdkInternalExecutionAttribute.HTTP_CHECKSUM))
.put(SIGNING_REGION, executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
.put(S3InternalSdkHttpExecutionAttribute.OBJECT_FILE_PATH,
executionAttributes.getAttribute(OBJECT_FILE_PATH))
.put(USE_S3_EXPRESS_AUTH, S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes))
.put(SIGNING_NAME, executionAttributes.getAttribute(SERVICE_SIGNING_NAME))
.put(REQUEST_CHECKSUM_CALCULATION,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.REQUEST_CHECKSUM_CALCULATION))
.put(RESPONSE_CHECKSUM_VALIDATION,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION))
.put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_PATH,
executionAttributes.getAttribute(RESPONSE_FILE_PATH))
.put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_OPTION,
executionAttributes.getAttribute(RESPONSE_FILE_OPTION));

SdkRequest request = context.request();
if (request instanceof AwsRequest) {
((AwsRequest) request).overrideConfiguration().ifPresent(config -> {
AwsRequestOverrideConfiguration awsConfig = (AwsRequestOverrideConfiguration) config;

Check warning on line 446 in services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast to "AwsRequestOverrideConfiguration".

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZzoGVVTg4hdzKgppjB2&open=AZzoGVVTg4hdzKgppjB2&pullRequest=6793
awsConfig.credentialsIdentityProvider().ifPresent(credentialsProvider -> {
CrtCredentialsProviderAdapter adapter =
new CrtCredentialsProviderAdapter(credentialsProvider);
builder.put(S3InternalSdkHttpExecutionAttribute.CRT_CREDENTIALS_PROVIDER_ADAPTER,
adapter);
});
});
}

SdkHttpExecutionAttributes attributes = builder.build();

// We rely on CRT to perform checksum validation, disable SDK flexible checksum implementation
executionAttributes.putAttribute(SdkInternalExecutionAttribute.HTTP_CHECKSUM, null);
Expand Down Expand Up @@ -468,11 +481,6 @@
throw new UnsupportedOperationException("Request-level signer override is not supported");
}

// TODO: support request-level credential override
if (overrideConfiguration.credentialsIdentityProvider().isPresent()) {
throw new UnsupportedOperationException("Request-level credentials override is not supported");
}

if (!CollectionUtils.isNullOrEmpty(overrideConfiguration.metricPublishers())) {
throw new UnsupportedOperationException("Request-level Metric Publishers override is not supported");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import software.amazon.awssdk.core.checksums.RequestChecksumCalculation;
import software.amazon.awssdk.core.checksums.ResponseChecksumValidation;
import software.amazon.awssdk.core.interceptor.trait.HttpChecksum;
import software.amazon.awssdk.crt.auth.credentials.CredentialsProvider;
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
import software.amazon.awssdk.crt.http.HttpHeader;
import software.amazon.awssdk.crt.http.HttpProxyEnvironmentVariableSetting;
Expand Down Expand Up @@ -186,6 +187,9 @@
requestOptions = requestOptions.withResponseFileOption(responseFileOption);
}

CrtCredentialsProviderAdapter requestCredentialsAdapter =
httpExecutionAttributes.getAttribute(S3InternalSdkHttpExecutionAttribute.CRT_CREDENTIALS_PROVIDER_ADAPTER);

try {
S3MetaRequestWrapper requestWrapper = new S3MetaRequestWrapper(crtS3Client.makeMetaRequest(requestOptions));
s3MetaRequestFuture.complete(requestWrapper);
Expand All @@ -196,16 +200,30 @@
if (observable != null) {
observable.subscribe(requestWrapper);
}
} catch (Throwable t) {

Check warning on line 203 in services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch Exception instead of Throwable.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZzoGVMKg4hdzKgppjB1&open=AZzoGVMKg4hdzKgppjB1&pullRequest=6793
if (requestCredentialsAdapter != null) {
requestCredentialsAdapter.close();
}
throw t;
} finally {
signingConfig.close();
}

if (requestCredentialsAdapter != null) {
executeFuture.whenComplete((result, error) -> requestCredentialsAdapter.close());
}

return executeFuture;
}

private AwsSigningConfig awsSigningConfig(Region signingRegion, SdkHttpExecutionAttributes httpExecutionAttributes) {
CrtCredentialsProviderAdapter requestAdapter =
httpExecutionAttributes.getAttribute(S3InternalSdkHttpExecutionAttribute.CRT_CREDENTIALS_PROVIDER_ADAPTER);
CredentialsProvider effectiveCredentials =
requestAdapter != null ? requestAdapter.crtCredentials() : s3ClientOptions.getCredentialsProvider();

AwsSigningConfig defaultS3SigningConfig =
AwsSigningConfig.getDefaultS3SigningConfig(s3ClientOptions.getRegion(), s3ClientOptions.getCredentialsProvider());
AwsSigningConfig.getDefaultS3SigningConfig(s3ClientOptions.getRegion(), effectiveCredentials);

// Override the region only if the signing region has changed from the previously configured region.
if (signingRegion != null && !s3ClientOptions.getRegion().equals(signingRegion.id())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public final class S3InternalSdkHttpExecutionAttribute<T> extends SdkHttpExecuti
public static final S3InternalSdkHttpExecutionAttribute<S3MetaRequestOptions.ResponseFileOption> RESPONSE_FILE_OPTION =
new S3InternalSdkHttpExecutionAttribute<>(S3MetaRequestOptions.ResponseFileOption.class);

public static final S3InternalSdkHttpExecutionAttribute<CrtCredentialsProviderAdapter> CRT_CREDENTIALS_PROVIDER_ADAPTER =
new S3InternalSdkHttpExecutionAttribute<>(CrtCredentialsProviderAdapter.class);

private S3InternalSdkHttpExecutionAttribute(Class<T> valueClass) {
super(valueClass);
}
Expand Down
Loading
Loading