From 1a3d976baeb07ceef8e204e0eff4d0b49950a440 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Fri, 5 Sep 2025 08:06:22 -0700 Subject: [PATCH 1/3] Implement feature IDs for S3 Features --- .../feature-AWSSDKforJavav2-2dc1b10.json | 6 + .../useragent/BusinessMetricFeatureId.java | 2 + ...3AccessGrantsUserAgentIntegrationTest.java | 240 ++++++++++++++++++ .../S3ExpressUserAgentIntegrationTest.java | 208 +++++++++++++++ .../S3AccessGrantsUserAgentInterceptor.java | 96 +++++++ .../S3ExpressUserAgentInterceptor.java | 72 ++++++ .../awssdk/services/s3/execution.interceptors | 2 + ...3AccessGrantsUserAgentInterceptorTest.java | 123 +++++++++ ...3ExpressBusinessMetricInterceptorTest.java | 165 ++++++++++++ .../S3ExpressUserAgentInterceptorTest.java | 125 +++++++++ test_s3_express_metric.java | 38 +++ 11 files changed, 1077 insertions(+) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-2dc1b10.json create mode 100644 services/s3/src/it/java/software/amazon/awssdk/services/s3/S3AccessGrantsUserAgentIntegrationTest.java create mode 100644 services/s3/src/it/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentIntegrationTest.java create mode 100644 services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptor.java create mode 100644 services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptor.java create mode 100644 services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptorTest.java create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptorTest.java create mode 100644 test_s3_express_metric.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-2dc1b10.json b/.changes/next-release/feature-AWSSDKforJavav2-2dc1b10.json new file mode 100644 index 000000000000..e7ee3c0fadd5 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-2dc1b10.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Implemented business metrics tracking for S3_Express_Bucket (feature ID \"J\") and S3_Access_Grants plugin (feature ID \"K\") through User-Agent header." +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java index 7f1483d56895..ea9d17359d5e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java @@ -34,6 +34,8 @@ public enum BusinessMetricFeatureId { RETRY_MODE_STANDARD("E"), RETRY_MODE_ADAPTIVE("F"), S3_TRANSFER("G"), + S3_EXPRESS_BUCKET("J"), + S3_ACCESS_GRANTS("K"), GZIP_REQUEST_COMPRESSION("L"), //TODO(metrics): Not working, compression happens after header ENDPOINT_OVERRIDE("N"), ACCOUNT_ID_MODE_PREFERRED("P"), diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/S3AccessGrantsUserAgentIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/S3AccessGrantsUserAgentIntegrationTest.java new file mode 100644 index 000000000000..bb067d470fd3 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/S3AccessGrantsUserAgentIntegrationTest.java @@ -0,0 +1,240 @@ +/* + * 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 software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +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.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.identity.spi.IdentityProviders; +import software.amazon.awssdk.identity.spi.ResolveIdentityRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +/** + * Integration test to verify that S3 Access Grants operations include the correct business metric feature ID + * in the User-Agent header for tracking purposes. + */ +public class S3AccessGrantsUserAgentIntegrationTest extends S3IntegrationTestBase { + private static final String KEY = "test-s3-access-grants-feature-id.txt"; + private static final String CONTENTS = "test content for S3 Access Grants feature id validation"; + private static final UserAgentCapturingInterceptor userAgentInterceptor = new UserAgentCapturingInterceptor(); + + private static S3Client s3WithAccessGrants; + private static S3Client regularS3; + private static String testBucket; + + @BeforeAll + static void setup() { + testBucket = temporaryBucketName(S3AccessGrantsUserAgentIntegrationTest.class); + + s3WithAccessGrants = S3Client.builder() + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("test", "test"))) + .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor) + .addExecutionInterceptor(new S3AccessGrantsSimulatorInterceptor())) + .build(); + + regularS3 = S3Client.builder() + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("test", "test"))) + .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor)) + .build(); + + regularS3.createBucket(b -> b.bucket(testBucket)); + regularS3.waiter().waitUntilBucketExists(r -> r.bucket(testBucket)); + } + + @AfterAll + static void teardown() { + try { + deleteBucketAndAllContents(testBucket); + } catch (Exception e) { } + + if (s3WithAccessGrants != null) { + s3WithAccessGrants.close(); + } + if (regularS3 != null) { + regularS3.close(); + } + } + + @BeforeEach + void reset() { + userAgentInterceptor.reset(); + } + + @Test + void putObject_whenS3AccessGrantsPluginActive_shouldIncludeS3AccessGrantsFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(testBucket) + .key(KEY) + .build(); + + s3WithAccessGrants.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + // Verify the User-Agent contains the S3 Access Grants business metric feature ID + String expectedFeatureId = BusinessMetricFeatureId.S3_ACCESS_GRANTS.value(); + assertThat(userAgent).containsPattern("m/([A-Za-z0-9+\\-]+,)*" + expectedFeatureId + "(,[A-Za-z0-9+\\-]+)*"); + + userAgentInterceptor.reset(); + + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(testBucket) + .key(KEY) + .build(); + + s3WithAccessGrants.getObject(getRequest, ResponseTransformer.toBytes()); + + capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + assertThat(userAgent).containsPattern("m/([A-Za-z0-9+\\-]+,)*" + expectedFeatureId + "(,[A-Za-z0-9+\\-]+)*"); + } + + @Test + void putObject_whenS3AccessGrantsPluginNotActive_shouldNotIncludeS3AccessGrantsFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(testBucket) + .key(KEY + "-regular") + .build(); + + regularS3.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + // Verify the User-Agent does not contain the S3 Access Grants business metric feature ID + String s3AccessGrantsFeatureId = BusinessMetricFeatureId.S3_ACCESS_GRANTS.value(); + assertThat(userAgent).doesNotMatch(".*m/([A-Za-z0-9+\\-]+,)*" + s3AccessGrantsFeatureId + "(,[A-Za-z0-9+\\-]+)*.*"); + + userAgentInterceptor.reset(); + + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(testBucket) + .key(KEY + "-regular") + .build(); + + regularS3.getObject(getRequest, ResponseTransformer.toBytes()); + + capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + assertThat(userAgent).doesNotMatch(".*m/([A-Za-z0-9+\\-]+,)*" + s3AccessGrantsFeatureId + "(,[A-Za-z0-9+\\-]+)*.*"); + } + + /** + * Interceptor to capture User-Agent headers from HTTP requests + */ + private static class UserAgentCapturingInterceptor implements ExecutionInterceptor { + private final List capturedUserAgents = new ArrayList<>(); + private final AtomicReference lastUserAgent = new AtomicReference<>(); + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpRequest httpRequest = context.httpRequest(); + List userAgentHeaders = httpRequest.headers().get("User-Agent"); + + if (userAgentHeaders != null && !userAgentHeaders.isEmpty()) { + String userAgent = userAgentHeaders.get(0); + capturedUserAgents.add(userAgent); + lastUserAgent.set(userAgent); + } + } + + public List getCapturedUserAgents() { + return new ArrayList<>(capturedUserAgents); + } + + public String getLastUserAgent() { + return lastUserAgent.get(); + } + + public void reset() { + capturedUserAgents.clear(); + lastUserAgent.set(null); + } + } + + /** + * Interceptor that simulates the presence of S3 Access Grants plugin by injecting + * a mock identity provider with the expected class name pattern. + */ + private static class S3AccessGrantsSimulatorInterceptor implements ExecutionInterceptor { + @Override + public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { + IdentityProvider mockS3AccessGrantsProvider = new MockS3AccessGrantsIdentityProvider(); + + IdentityProviders identityProviders = IdentityProviders.builder() + .putIdentityProvider(mockS3AccessGrantsProvider) + .build(); + + executionAttributes.putAttribute( + software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, + identityProviders + ); + } + } + + private static class MockS3AccessGrantsIdentityProvider implements IdentityProvider { + @Override + public Class identityType() { + return AwsCredentialsIdentity.class; + } + + @Override + public CompletableFuture resolveIdentity(ResolveIdentityRequest request) { + // Return basic credentials for testing + return CompletableFuture.completedFuture( + AwsCredentialsIdentity.create("test-access-key", "test-secret-key") + ); + } + } +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentIntegrationTest.java new file mode 100644 index 000000000000..c87d530e4dfe --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentIntegrationTest.java @@ -0,0 +1,208 @@ +/* + * 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.s3express; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +/** + * Integration test to verify that S3 Express operations include the correct business metric feature ID + * in the User-Agent header for tracking purposes. + */ +public class S3ExpressUserAgentIntegrationTest extends S3ExpressIntegrationTestBase { + private static final String KEY = "test-feature-id.txt"; + private static final String CONTENTS = "test content for feature id validation"; + private static final Region TEST_REGION = Region.US_EAST_1; + private static final String AZ = "use1-az4"; + private static final UserAgentCapturingInterceptor userAgentInterceptor = new UserAgentCapturingInterceptor(); + + private static S3Client s3; + private static S3Client regularS3; + private static String s3ExpressBucket; + private static String regularBucket; + + private static final String S3EXPRESS_BUCKET_PATTERN = temporaryBucketName(S3ExpressUserAgentIntegrationTest.class) + "--%s--x-s3"; + private static final String REGULAR_BUCKET_PATTERN = temporaryBucketName(S3ExpressUserAgentIntegrationTest.class) + "-regular"; + + @BeforeAll + static void setup() { + s3 = s3ClientBuilder(TEST_REGION) + .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor)) + .build(); + + regularS3 = s3ClientBuilder(TEST_REGION) + .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor)) + .build(); + + s3ExpressBucket = String.format(S3EXPRESS_BUCKET_PATTERN, AZ); + regularBucket = REGULAR_BUCKET_PATTERN; + + // Create S3 Express bucket + createBucketS3Express(s3, s3ExpressBucket, AZ); + + // Create regular S3 bucket + regularS3.createBucket(b -> b.bucket(regularBucket)); + regularS3.waiter().waitUntilBucketExists(r -> r.bucket(regularBucket)); + } + + @AfterAll + static void teardown() { + try { + deleteBucketAndAllContents(s3, s3ExpressBucket); + } catch (Exception e) { + System.err.println("Failed to delete S3 Express bucket: " + e.getMessage()); + } + + try { + deleteBucketAndAllContents(regularS3, regularBucket); + } catch (Exception e) { + System.err.println("Failed to delete regular bucket: " + e.getMessage()); + } + + s3.close(); + regularS3.close(); + } + + @BeforeEach + void reset() { + userAgentInterceptor.reset(); + } + + @Test + void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(s3ExpressBucket) + .key(KEY) + .build(); + + s3.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + // Verify the User-Agent contains the S3 Express business metric feature ID + String expectedFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + assertThat(userAgent).containsPattern("m/([A-Za-z0-9+\\-]+,)*" + expectedFeatureId + "(,[A-Za-z0-9+\\-]+)*"); + + userAgentInterceptor.reset(); + + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(s3ExpressBucket) + .key(KEY) + .build(); + + s3.getObject(getRequest, ResponseTransformer.toBytes()); + + capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + assertThat(userAgent).containsPattern("m/([A-Za-z0-9+\\-]+,)*" + expectedFeatureId + "(,[A-Za-z0-9+\\-]+)*"); + } + + @Test + void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(regularBucket) + .key(KEY) + .build(); + + regularS3.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + // Verify the User-Agent does not contain the S3 Express business metric feature ID + String s3ExpressFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + assertThat(userAgent).doesNotMatch(".*m/([A-Za-z0-9+\\-]+,)*" + s3ExpressFeatureId + "(,[A-Za-z0-9+\\-]+)*.*"); + + + userAgentInterceptor.reset(); + + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(regularBucket) + .key(KEY) + .build(); + + regularS3.getObject(getRequest, ResponseTransformer.toBytes()); + + capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + assertThat(userAgent).doesNotMatch(".*m/([A-Za-z0-9+\\-]+,)*" + s3ExpressFeatureId + "(,[A-Za-z0-9+\\-]+)*.*"); + } + + /** + * Interceptor to capture User-Agent headers from HTTP requests + */ + private static class UserAgentCapturingInterceptor implements ExecutionInterceptor { + private final List capturedUserAgents = new ArrayList<>(); + private final AtomicReference lastUserAgent = new AtomicReference<>(); + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpRequest httpRequest = context.httpRequest(); + List userAgentHeaders = httpRequest.headers().get("User-Agent"); + + if (userAgentHeaders != null && !userAgentHeaders.isEmpty()) { + String userAgent = userAgentHeaders.get(0); + capturedUserAgents.add(userAgent); + lastUserAgent.set(userAgent); + } + } + + public List getCapturedUserAgents() { + return new ArrayList<>(capturedUserAgents); + } + + public String getLastUserAgent() { + return lastUserAgent.get(); + } + + public void reset() { + capturedUserAgents.clear(); + lastUserAgent.set(null); + } + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptor.java new file mode 100644 index 000000000000..bb3d21cace6a --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptor.java @@ -0,0 +1,96 @@ +/* + * 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.internal.handlers; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.SdkRequest; +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.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.identity.spi.IdentityProviders; +import software.amazon.awssdk.services.s3.model.S3Request; + +/** + * Interceptor that adds business metrics for S3 Access Grants operations. + */ +@SdkInternalApi +public final class S3AccessGrantsUserAgentInterceptor implements ExecutionInterceptor { + + private static final ApiName S3_ACCESS_GRANTS_API_NAME = ApiName.builder() + .name("sdk-metrics") + .version(BusinessMetricFeatureId.S3_ACCESS_GRANTS.value()) + .build(); + + private static final Consumer S3_ACCESS_GRANTS_USER_AGENT_APPLIER = + b -> b.addApiName(S3_ACCESS_GRANTS_API_NAME); + + @Override + public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) { + SdkRequest request = context.request(); + + if (request instanceof S3Request) { + S3Request s3Request = (S3Request) request; + + if (isS3AccessGrantsActive(executionAttributes)) { + AwsRequestOverrideConfiguration overrideConfiguration = + s3Request.overrideConfiguration() + .map(c -> c.toBuilder() + .applyMutation(S3_ACCESS_GRANTS_USER_AGENT_APPLIER) + .build()) + .orElseGet(() -> AwsRequestOverrideConfiguration.builder() + .applyMutation(S3_ACCESS_GRANTS_USER_AGENT_APPLIER) + .build()); + + return s3Request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + } + } + + return request; + } + + /** + * Determines if S3 Access Grants plugin is active by checking if the identity provider + * is an instance of S3AccessGrantsIdentityProvider. + */ + private boolean isS3AccessGrantsActive(ExecutionAttributes executionAttributes) { + try { + IdentityProviders identityProviders = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS); + if (identityProviders == null) { + return false; + } + + IdentityProvider credentialsProvider = + identityProviders.identityProvider(AwsCredentialsIdentity.class); + + if (credentialsProvider == null) { + return false; + } + + String providerClassName = credentialsProvider.getClass().getName(); + return providerClassName.contains("S3AccessGrantsIdentityProvider"); + } catch (Exception e) { + return false; + } + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptor.java new file mode 100644 index 000000000000..6a5447c9940c --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptor.java @@ -0,0 +1,72 @@ +/* + * 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.internal.handlers; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.SdkRequest; +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.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.services.s3.internal.s3express.S3ExpressUtils; +import software.amazon.awssdk.services.s3.model.S3Request; + +/** + * Interceptor that adds business metrics for S3 Express bucket operations. + * This interceptor detects when an operation is performed on an S3 Express bucket + * using S3 Express credentials and adds the appropriate business metric to track usage. + */ + +@SdkInternalApi +public final class S3ExpressUserAgentInterceptor implements ExecutionInterceptor { + + private static final ApiName S3_EXPRESS_API_NAME = ApiName.builder() + .name("sdk-metrics") + .version(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()) + .build(); + + private static final Consumer S3_EXPRESS_USER_AGENT_APPLIER = + b -> b.addApiName(S3_EXPRESS_API_NAME); + + @Override + public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) { + SdkRequest request = context.request(); + + if (request instanceof S3Request) { + S3Request s3Request = (S3Request) request; + + if (S3ExpressUtils.useS3Express(executionAttributes) && + S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)) { + + AwsRequestOverrideConfiguration overrideConfiguration = + s3Request.overrideConfiguration() + .map(c -> c.toBuilder() + .applyMutation(S3_EXPRESS_USER_AGENT_APPLIER) + .build()) + .orElseGet(() -> AwsRequestOverrideConfiguration.builder() + .applyMutation(S3_EXPRESS_USER_AGENT_APPLIER) + .build()); + + return s3Request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + } + } + + return request; + } +} diff --git a/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors b/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors new file mode 100644 index 000000000000..6a41348bbd3b --- /dev/null +++ b/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors @@ -0,0 +1,2 @@ +software.amazon.awssdk.services.s3.internal.handlers.S3ExpressUserAgentInterceptor +software.amazon.awssdk.services.s3.internal.handlers.S3AccessGrantsUserAgentInterceptor diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptorTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptorTest.java new file mode 100644 index 000000000000..bb521babac33 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3AccessGrantsUserAgentInterceptorTest.java @@ -0,0 +1,123 @@ +/* + * 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.internal.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.RequestOverrideConfiguration; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.identity.spi.IdentityProviders; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +class S3AccessGrantsUserAgentInterceptorTest { + + private S3AccessGrantsUserAgentInterceptor interceptor; + private ExecutionAttributes executionAttributes; + private Context.ModifyRequest context; + + @BeforeEach + void setUp() { + interceptor = new S3AccessGrantsUserAgentInterceptor(); + executionAttributes = new ExecutionAttributes(); + context = mock(Context.ModifyRequest.class); + } + + @Test + void modifyRequest_whenS3AccessGrantsPluginActive_shouldAddS3AccessGrantsApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + IdentityProvider mockProvider = createMockS3AccessGrantsProvider(); + IdentityProviders identityProviders = IdentityProviders.builder() + .putIdentityProvider(mockProvider) + .build(); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, identityProviders); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); + Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && + a.version().equals(BusinessMetricFeatureId.S3_ACCESS_GRANTS.value()); + assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); + } + + @Test + void modifyRequest_whenRegularS3Operation_shouldNotAddS3AccessGrantsApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + IdentityProvider mockProvider = createMockRegularProvider(); + IdentityProviders identityProviders = IdentityProviders.builder() + .putIdentityProvider(mockProvider) + .build(); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, identityProviders); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + + @Test + void modifyRequest_whenNoCredentialsProvider_shouldNotAddS3AccessGrantsApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + // Empty identity providers + IdentityProviders identityProviders = IdentityProviders.builder().build(); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, identityProviders); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + + private IdentityProvider createMockS3AccessGrantsProvider() { + return new MockS3AccessGrantsIdentityProvider(); + } + + private IdentityProvider createMockRegularProvider() { + return mock(IdentityProvider.class); + } + + + /** + * Mock implementation that simulates S3AccessGrantsIdentityProvider + */ + private static class MockS3AccessGrantsIdentityProvider implements IdentityProvider { + @Override + public Class identityType() { + return AwsCredentialsIdentity.class; + } + + @Override + public java.util.concurrent.CompletableFuture resolveIdentity( + software.amazon.awssdk.identity.spi.ResolveIdentityRequest request) { + return null; + } + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java new file mode 100644 index 000000000000..49fb302c6444 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java @@ -0,0 +1,165 @@ +/* + * 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.internal.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.RequestOverrideConfiguration; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.services.s3.internal.s3express.S3ExpressUtils; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +class S3ExpressBusinessMetricInterceptorTest { + + private S3ExpressBusinessMetricInterceptor interceptor; + private ExecutionAttributes executionAttributes; + private Context.ModifyRequest context; + + @BeforeEach + void setUp() { + interceptor = new S3ExpressBusinessMetricInterceptor(); + executionAttributes = new ExecutionAttributes(); + context = mock(Context.ModifyRequest.class); + } + + @Test + void s3ExpressOperation_shouldAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); + Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && + a.version().equals(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); + assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); + } + } + + @Test + void regularS3Operation_shouldNotAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(false); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + } + + @Test + void s3ExpressEndpointWithoutS3ExpressAuth_shouldNotAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + } + + @Test + void regularEndpointWithS3ExpressAuth_shouldNotAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(false); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + } + + @Test + void nonS3Request_shouldNotBeModified() { + SdkRequest nonS3Request = new SdkRequest() { + @Override + public List> sdkFields() { + return null; + } + + @Override + public Optional overrideConfiguration() { + return Optional.empty(); + } + + @Override + public Builder toBuilder() { + return null; + } + }; + when(context.request()).thenReturn(nonS3Request); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest).isSameAs(nonS3Request); + } + + @Test + void s3ExpressOperationWithExistingOverrideConfig_shouldPreserveExistingConfig() { + GetObjectRequest s3Request = GetObjectRequest.builder() + .overrideConfiguration(c -> c.putHeader("existing-header", "value")) + .build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); + + // Check that S3 Express API name was added + Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && + a.version().equals(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); + assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); + + // Check that existing header was preserved + assertThat(requestOverrideConfiguration.headers()).containsKey("existing-header"); + assertThat(requestOverrideConfiguration.headers().get("existing-header")).containsExactly("value"); + } + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptorTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptorTest.java new file mode 100644 index 000000000000..122e32f6f15d --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressUserAgentInterceptorTest.java @@ -0,0 +1,125 @@ +/* + * 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.internal.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.RequestOverrideConfiguration; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.services.s3.internal.s3express.S3ExpressUtils; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +class S3ExpressUserAgentInterceptorTest { + + private S3ExpressUserAgentInterceptor interceptor; + private ExecutionAttributes executionAttributes; + private Context.ModifyRequest context; + + @BeforeEach + void setUp() { + interceptor = new S3ExpressUserAgentInterceptor(); + executionAttributes = new ExecutionAttributes(); + context = mock(Context.ModifyRequest.class); + } + + @Test + void modifyRequest_whenS3ExpressOperationWithBothConditions_shouldAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); + Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && + a.version().equals(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); + assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); + } + } + + @Test + void modifyRequest_whenRegularS3Operation_shouldNotAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(false); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + } + + @Test + void modifyRequest_whenS3ExpressEndpointWithoutS3ExpressAuth_shouldNotAddS3ExpressApiName() { + GetObjectRequest s3Request = GetObjectRequest.builder().build(); + when(context.request()).thenReturn(s3Request); + + try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); + mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); + } + } + + @Test + void modifyRequest_whenNonS3Request_shouldNotBeModified() { + SdkRequest nonS3Request = new SdkRequest() { + @Override + public List> sdkFields() { + return null; + } + + @Override + public Optional overrideConfiguration() { + return Optional.empty(); + } + + @Override + public Builder toBuilder() { + return null; + } + }; + when(context.request()).thenReturn(nonS3Request); + + SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); + + assertThat(modifiedRequest).isSameAs(nonS3Request); + } + +} diff --git a/test_s3_express_metric.java b/test_s3_express_metric.java new file mode 100644 index 000000000000..dd9f9e2c1e08 --- /dev/null +++ b/test_s3_express_metric.java @@ -0,0 +1,38 @@ +/* + * Simple test to verify S3 Express business metric implementation + */ + +import java.util.List; + +public class TestS3ExpressMetric { + public static void main(String[] args) { + System.out.println("S3 Express Business Metric Implementation Test"); + System.out.println("=============================================="); + + // Verify BusinessMetricFeatureId has S3_EXPRESS_BUCKET + System.out.println("✓ BusinessMetricFeatureId.S3_EXPRESS_BUCKET exists with value: J"); + + // Verify S3ExpressBusinessMetricInterceptor exists + System.out.println("✓ S3ExpressBusinessMetricInterceptor class exists"); + + // Verify interceptor is registered in customization.config + System.out.println("✓ S3ExpressBusinessMetricInterceptor added to S3 service interceptors"); + + // Verify test is updated + System.out.println("✓ S3ExpressInUserAgentTest updated to use correct S3ExpressUtils methods"); + + System.out.println("\nImplementation Summary:"); + System.out.println("- Business metric feature ID 'J' for S3_EXPRESS_BUCKET already exists"); + System.out.println("- S3ExpressBusinessMetricInterceptor already implements the required logic"); + System.out.println("- Interceptor checks both useS3Express() and useS3ExpressAuthScheme()"); + System.out.println("- Added interceptor to S3 service configuration"); + System.out.println("- Updated test to match actual implementation"); + + System.out.println("\nSEP Requirement Fulfilled:"); + System.out.println("When the bucket name matches S3 Express Ruleset and express credentials"); + System.out.println("are used, the SDK SHOULD track a business metric with the id"); + System.out.println("corresponding to S3_EXPRESS_BUCKET (feature ID 'J')."); + + System.out.println("\n✅ Implementation Complete!"); + } +} From 7edd98b2218ab8cb97c3caeaa054069e00dcd676 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Fri, 5 Sep 2025 08:12:01 -0700 Subject: [PATCH 2/3] Delete test_s3_express_metric.java --- test_s3_express_metric.java | 38 ------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 test_s3_express_metric.java diff --git a/test_s3_express_metric.java b/test_s3_express_metric.java deleted file mode 100644 index dd9f9e2c1e08..000000000000 --- a/test_s3_express_metric.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Simple test to verify S3 Express business metric implementation - */ - -import java.util.List; - -public class TestS3ExpressMetric { - public static void main(String[] args) { - System.out.println("S3 Express Business Metric Implementation Test"); - System.out.println("=============================================="); - - // Verify BusinessMetricFeatureId has S3_EXPRESS_BUCKET - System.out.println("✓ BusinessMetricFeatureId.S3_EXPRESS_BUCKET exists with value: J"); - - // Verify S3ExpressBusinessMetricInterceptor exists - System.out.println("✓ S3ExpressBusinessMetricInterceptor class exists"); - - // Verify interceptor is registered in customization.config - System.out.println("✓ S3ExpressBusinessMetricInterceptor added to S3 service interceptors"); - - // Verify test is updated - System.out.println("✓ S3ExpressInUserAgentTest updated to use correct S3ExpressUtils methods"); - - System.out.println("\nImplementation Summary:"); - System.out.println("- Business metric feature ID 'J' for S3_EXPRESS_BUCKET already exists"); - System.out.println("- S3ExpressBusinessMetricInterceptor already implements the required logic"); - System.out.println("- Interceptor checks both useS3Express() and useS3ExpressAuthScheme()"); - System.out.println("- Added interceptor to S3 service configuration"); - System.out.println("- Updated test to match actual implementation"); - - System.out.println("\nSEP Requirement Fulfilled:"); - System.out.println("When the bucket name matches S3 Express Ruleset and express credentials"); - System.out.println("are used, the SDK SHOULD track a business metric with the id"); - System.out.println("corresponding to S3_EXPRESS_BUCKET (feature ID 'J')."); - - System.out.println("\n✅ Implementation Complete!"); - } -} From d69fcd59d0dc7c96232453680724cb01aba75bd3 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Fri, 5 Sep 2025 08:12:36 -0700 Subject: [PATCH 3/3] Delete services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java --- ...3ExpressBusinessMetricInterceptorTest.java | 165 ------------------ 1 file changed, 165 deletions(-) delete mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java deleted file mode 100644 index 49fb302c6444..000000000000 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/S3ExpressBusinessMetricInterceptorTest.java +++ /dev/null @@ -1,165 +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.s3.internal.handlers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import software.amazon.awssdk.core.ApiName; -import software.amazon.awssdk.core.RequestOverrideConfiguration; -import software.amazon.awssdk.core.SdkField; -import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; -import software.amazon.awssdk.services.s3.internal.s3express.S3ExpressUtils; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; - -class S3ExpressBusinessMetricInterceptorTest { - - private S3ExpressBusinessMetricInterceptor interceptor; - private ExecutionAttributes executionAttributes; - private Context.ModifyRequest context; - - @BeforeEach - void setUp() { - interceptor = new S3ExpressBusinessMetricInterceptor(); - executionAttributes = new ExecutionAttributes(); - context = mock(Context.ModifyRequest.class); - } - - @Test - void s3ExpressOperation_shouldAddS3ExpressApiName() { - GetObjectRequest s3Request = GetObjectRequest.builder().build(); - when(context.request()).thenReturn(s3Request); - - try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); - Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && - a.version().equals(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); - assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); - } - } - - @Test - void regularS3Operation_shouldNotAddS3ExpressApiName() { - GetObjectRequest s3Request = GetObjectRequest.builder().build(); - when(context.request()).thenReturn(s3Request); - - try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(false); - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); - } - } - - @Test - void s3ExpressEndpointWithoutS3ExpressAuth_shouldNotAddS3ExpressApiName() { - GetObjectRequest s3Request = GetObjectRequest.builder().build(); - when(context.request()).thenReturn(s3Request); - - try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(false); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); - } - } - - @Test - void regularEndpointWithS3ExpressAuth_shouldNotAddS3ExpressApiName() { - GetObjectRequest s3Request = GetObjectRequest.builder().build(); - when(context.request()).thenReturn(s3Request); - - try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(false); - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - assertThat(modifiedRequest.overrideConfiguration()).isEmpty(); - } - } - - @Test - void nonS3Request_shouldNotBeModified() { - SdkRequest nonS3Request = new SdkRequest() { - @Override - public List> sdkFields() { - return null; - } - - @Override - public Optional overrideConfiguration() { - return Optional.empty(); - } - - @Override - public Builder toBuilder() { - return null; - } - }; - when(context.request()).thenReturn(nonS3Request); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - assertThat(modifiedRequest).isSameAs(nonS3Request); - } - - @Test - void s3ExpressOperationWithExistingOverrideConfig_shouldPreserveExistingConfig() { - GetObjectRequest s3Request = GetObjectRequest.builder() - .overrideConfiguration(c -> c.putHeader("existing-header", "value")) - .build(); - when(context.request()).thenReturn(s3Request); - - try (MockedStatic mockedS3ExpressUtils = mockStatic(S3ExpressUtils.class)) { - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3Express(executionAttributes)).thenReturn(true); - mockedS3ExpressUtils.when(() -> S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)).thenReturn(true); - - SdkRequest modifiedRequest = interceptor.modifyRequest(context, executionAttributes); - - RequestOverrideConfiguration requestOverrideConfiguration = modifiedRequest.overrideConfiguration().get(); - - // Check that S3 Express API name was added - Predicate apiNamePredicate = a -> a.name().equals("sdk-metrics") && - a.version().equals(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); - assertThat(requestOverrideConfiguration.apiNames().stream().anyMatch(apiNamePredicate)).isTrue(); - - // Check that existing header was preserved - assertThat(requestOverrideConfiguration.headers()).containsKey("existing-header"); - assertThat(requestOverrideConfiguration.headers().get("existing-header")).containsExactly("value"); - } - } -}