diff --git a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json new file mode 100644 index 000000000000..48c75e006610 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon S3", + "contributor": "", + "description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index 7bac753e88e5..83c81b97ad7d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -921,6 +921,12 @@ private MethodSpec setMetricValuesMethod() { + "metrics -> endpoint.attribute($T.METRIC_VALUES).forEach(v -> metrics.addMetric(v)))", SdkInternalExecutionAttribute.class, AwsEndpointAttribute.class); b.endControlFlow(); + + if (endpointRulesSpecUtils.isS3()) { + b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(executionAttributes)", + ClassName.get("software.amazon.awssdk.services.s3.internal.s3express", "S3ExpressUtils")); + } + return b.build(); } } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ChildProfileCredentialsProviderFactory.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ChildProfileCredentialsProviderFactory.java index 620e32decfe2..e91352f3387f 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ChildProfileCredentialsProviderFactory.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ChildProfileCredentialsProviderFactory.java @@ -40,5 +40,23 @@ public interface ChildProfileCredentialsProviderFactory { * provider. * @return The credentials provider with permissions derived from the source credentials provider and profile. */ - AwsCredentialsProvider create(AwsCredentialsProvider sourceCredentialsProvider, Profile profile); + default AwsCredentialsProvider create(AwsCredentialsProvider sourceCredentialsProvider, Profile profile) { + return create(sourceCredentialsProvider, profile, null); + } + + /** + * Create a credentials provider for the provided profile, using the provided source credentials provider to authenticate + * with AWS. In the case of STS, the returned credentials provider is for a role that has been assumed, and the provided + * source credentials provider is the credentials that should be used to authenticate that the user is allowed to assume + * that role. + * + * @param sourceCredentialsProvider The credentials provider that should be used to authenticate the child credentials + * provider. This credentials provider should be closed when it is no longer used. + * @param profile The profile that should be used to load the configuration necessary to create the child credentials + * provider. + * @param source A string list of {@link software.amazon.awssdk.core.useragent.BusinessMetricFeatureId} denoting + * previous credentials providers that are chained with this one. + * @return The credentials provider with permissions derived from the source credentials provider and profile. + */ + AwsCredentialsProvider create(AwsCredentialsProvider sourceCredentialsProvider, Profile profile, String source); } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java index efec7ffce6bd..2879a3070443 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java @@ -39,6 +39,7 @@ import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.core.util.SdkUserAgent; import software.amazon.awssdk.regions.util.ResourcesEndpointProvider; import software.amazon.awssdk.regions.util.ResourcesEndpointRetryPolicy; @@ -72,7 +73,8 @@ public final class ContainerCredentialsProvider implements HttpCredentialsProvider, ToCopyableBuilder { - private static final String PROVIDER_NAME = "ContainerCredentialsProvider"; + private static final String CLASS_NAME = "ContainerCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_HTTP.value(); private static final Predicate IS_LOOPBACK_ADDRESS = InetAddress::isLoopbackAddress; private static final Predicate ALLOWED_HOSTS_RULES = IS_LOOPBACK_ADDRESS; private static final String HTTPS = "https"; @@ -90,6 +92,7 @@ public final class ContainerCredentialsProvider private final Boolean asyncCredentialUpdateEnabled; private final String asyncThreadName; + private final String source; /** * @see #builder() @@ -98,7 +101,8 @@ private ContainerCredentialsProvider(BuilderImpl builder) { this.endpoint = builder.endpoint; this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; this.asyncThreadName = builder.asyncThreadName; - this.httpCredentialsLoader = HttpCredentialsLoader.create(PROVIDER_NAME); + this.source = builder.source; + this.httpCredentialsLoader = HttpCredentialsLoader.create(providerName()); if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) { Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName"); @@ -126,7 +130,7 @@ public static Builder builder() { @Override public String toString() { - return ToString.create(PROVIDER_NAME); + return ToString.create(CLASS_NAME); } private RefreshResult refreshCredentials() { @@ -160,6 +164,14 @@ private Instant prefetchTime(Instant expiration) { return ComparableUtils.minimum(oneHourFromNow, fifteenMinutesBeforeExpiration); } + private String providerName() { + String providerName = PROVIDER_NAME; + if (source != null && !source.isEmpty()) { + providerName = String.format("%s,%s", source, providerName); + } + return providerName; + } + @Override public AwsCredentials resolveCredentials() { return credentialsCache.get(); @@ -318,6 +330,7 @@ private static final class BuilderImpl implements Builder { private String endpoint; private Boolean asyncCredentialUpdateEnabled; private String asyncThreadName; + private String source; private BuilderImpl() { asyncThreadName("container-credentials-provider"); @@ -327,6 +340,7 @@ private BuilderImpl(ContainerCredentialsProvider credentialsProvider) { this.endpoint = credentialsProvider.endpoint; this.asyncCredentialUpdateEnabled = credentialsProvider.asyncCredentialUpdateEnabled; this.asyncThreadName = credentialsProvider.asyncThreadName; + this.source = credentialsProvider.source; } @Override @@ -359,6 +373,16 @@ public void setAsyncThreadName(String asyncThreadName) { asyncThreadName(asyncThreadName); } + @Override + public Builder source(String source) { + this.source = source; + return this; + } + + public void setSource(String source) { + source(source); + } + @Override public ContainerCredentialsProvider build() { return new ContainerCredentialsProvider(this); diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/EnvironmentVariableCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/EnvironmentVariableCredentialsProvider.java index e05c24eed05a..f7eb0df32e6b 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/EnvironmentVariableCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/EnvironmentVariableCredentialsProvider.java @@ -18,6 +18,7 @@ import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.auth.credentials.internal.SystemSettingsCredentialsProvider; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.utils.SystemSetting; import software.amazon.awssdk.utils.ToString; @@ -28,7 +29,8 @@ @SdkPublicApi public final class EnvironmentVariableCredentialsProvider extends SystemSettingsCredentialsProvider { - private static final String PROVIDER_NAME = "EnvironmentVariableCredentialsProvider"; + private static final String CLASS_NAME = "EnvironmentVariableCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_ENV_VARS.value(); private EnvironmentVariableCredentialsProvider() { } @@ -52,6 +54,6 @@ protected String provider() { @Override public String toString() { - return ToString.create(PROVIDER_NAME); + return ToString.create(CLASS_NAME); } } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/HttpCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/HttpCredentialsProvider.java index ccc7e7aa7101..b93d23606ad4 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/HttpCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/HttpCredentialsProvider.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.auth.credentials; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.utils.SdkAutoCloseable; /** @@ -48,6 +49,14 @@ interface Builder { private static final Logger log = Logger.loggerFor(InstanceProfileCredentialsProvider.class); - private static final String PROVIDER_NAME = "InstanceProfileCredentialsProvider"; + private static final String CLASS_NAME = "InstanceProfileCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_IMDS.value(); private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token"; private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/"; private static final String TOKEN_RESOURCE = "/latest/api/token"; @@ -90,6 +92,8 @@ public final class InstanceProfileCredentialsProvider private final Duration staleTime; + private final String source; + /** * @see #builder() */ @@ -102,8 +106,9 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) { .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile())); this.profileName = Optional.ofNullable(builder.profileName) .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow); + this.source = builder.source; - this.httpCredentialsLoader = HttpCredentialsLoader.create(PROVIDER_NAME); + this.httpCredentialsLoader = HttpCredentialsLoader.create(providerName()); this.configProvider = Ec2MetadataConfigProvider.builder() .profileFile(profileFile) @@ -202,9 +207,17 @@ public void close() { credentialsCache.close(); } + private String providerName() { + String providerName = PROVIDER_NAME; + if (source != null && !source.isEmpty()) { + providerName = String.format("%s,%s", source, providerName); + } + return providerName; + } + @Override public String toString() { - return ToString.create(PROVIDER_NAME); + return ToString.create(CLASS_NAME); } private ResourcesEndpointProvider createEndpointProvider() { @@ -372,6 +385,7 @@ static final class BuilderImpl implements Builder { private Supplier profileFile; private String profileName; private Duration staleTime; + private String source; private BuilderImpl() { asyncThreadName("instance-profile-credentials-provider"); @@ -385,6 +399,7 @@ private BuilderImpl(InstanceProfileCredentialsProvider provider) { this.profileFile = provider.profileFile; this.profileName = provider.profileName; this.staleTime = provider.staleTime; + this.source = provider.source; } Builder clock(Clock clock) { @@ -463,6 +478,16 @@ public void setStaleTime(Duration duration) { staleTime(duration); } + @Override + public Builder source(String source) { + this.source = source; + return this; + } + + public void setSource(String source) { + source(source); + } + @Override public InstanceProfileCredentialsProvider build() { return new InstanceProfileCredentialsProvider(this); diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java index e27d511d0887..5b45b7671334 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.protocols.jsoncore.JsonNode; import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; import software.amazon.awssdk.utils.DateUtils; @@ -64,7 +65,8 @@ public final class ProcessCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable, ToCopyableBuilder { - private static final String PROVIDER_NAME = "ProcessCredentialsProvider"; + private static final String CLASS_NAME = "ProcessCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_PROCESS.value(); private static final JsonNodeParser PARSER = JsonNodeParser.builder() .removeErrorLocations(true) .build(); @@ -82,6 +84,8 @@ public final class ProcessCredentialsProvider private final Boolean asyncCredentialUpdateEnabled; + private final String source; + /** * @see #builder() */ @@ -93,6 +97,7 @@ private ProcessCredentialsProvider(Builder builder) { this.commandAsListOfStringsFromBuilder = builder.commandAsListOfStrings; this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; this.staticAccountId = builder.staticAccountId; + this.source = builder.source; CachedSupplier.Builder cacheBuilder = CachedSupplier.builder(this::refreshCredentials) .cachedValueName(toString()); @@ -171,6 +176,14 @@ private JsonNode parseProcessOutput(String processOutput) { return credentialsJson; } + private String providerName() { + String providerName = PROVIDER_NAME; + if (source != null && !source.isEmpty()) { + providerName = String.format("%s,%s", source, providerName); + } + return providerName; + } + /** * Parse the process output to retrieve the credentials. */ @@ -192,13 +205,13 @@ private AwsCredentials credentials(JsonNode credentialsJson) { .sessionToken(sessionToken) .expirationTime(credentialExpirationTime(credentialsJson)) .accountId(resolvedAccountId) - .providerName(PROVIDER_NAME) + .providerName(providerName()) .build() : AwsBasicCredentials.builder() .accessKeyId(accessKeyId) .secretAccessKey(secretAccessKey) .accountId(resolvedAccountId) - .providerName(PROVIDER_NAME) + .providerName(providerName()) .build(); } @@ -270,6 +283,7 @@ public static class Builder implements CopyableBuilder expectedValue, @@ -143,7 +150,7 @@ public void staticCredentials_commandAsListOfStrings_CanBeLoaded() { assertThat(credentials).isInstanceOf(AwsBasicCredentials.class); assertThat(credentials.accessKeyId()).isEqualTo("accessKeyId"); assertThat(credentials.secretAccessKey()).isEqualTo("secretAccessKey"); - assertThat(credentials.providerName()).isPresent().contains("ProcessCredentialsProvider"); + assertThat(credentials.providerName()).isPresent().hasValue(BusinessMetricFeatureId.CREDENTIALS_PROCESS.value()); } @Test @@ -186,11 +193,13 @@ void sessionCredentialsWithStaticAccountIdCanBeLoaded() { scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY, expiration)) .credentialRefreshThreshold(Duration.ofSeconds(1)) .staticAccountId("staticAccountId") + .source("v") .build(); AwsCredentials credentials = credentialsProvider.resolveCredentials(); verifySessionCredentials(credentials, expiration); assertThat(credentials.accountId()).isPresent().hasValue("staticAccountId"); + assertThat(credentials.providerName()).isPresent().hasValue("v,w"); } private void verifySessionCredentials(AwsCredentials credentials, String expiration) { diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/StaticCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/StaticCredentialsProviderTest.java index d02b633dfd2e..5502610543b1 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/StaticCredentialsProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/StaticCredentialsProviderTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; class StaticCredentialsProviderTest { @Test @@ -39,6 +40,7 @@ void getAwsCredentialsWithAccountId_ReturnsSameCredentials() { .build(); AwsCredentials actualCredentials = StaticCredentialsProvider.create(credentials).resolveCredentials(); assertThat(actualCredentials).isEqualTo(credentials); + assertThat(actualCredentials.providerName()).isPresent().contains(BusinessMetricFeatureId.CREDENTIALS_CODE.value()); } @@ -48,7 +50,7 @@ void getSessionAwsCredentials_ReturnsSameCredentials() { AwsCredentials actualCredentials = StaticCredentialsProvider.create(credentials).resolveCredentials(); assertThat(credentials).isEqualTo(actualCredentials); assertThat(credentials.providerName()).isNotPresent(); - assertThat(actualCredentials.providerName()).isPresent(); + assertThat(actualCredentials.providerName()).isPresent().contains(BusinessMetricFeatureId.CREDENTIALS_CODE.value()); } @Test diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingCredentialsProvidersTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingCredentialsProvidersTest.java index 8961c5d0a18c..9ecc620a5af4 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingCredentialsProvidersTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingCredentialsProvidersTest.java @@ -22,13 +22,16 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; import software.amazon.awssdk.utils.Pair; @@ -66,7 +69,9 @@ void configureEnvVars_resolveCredentials(String description, configureEnvironmentVariables(systemSettings); EnvironmentVariableCredentialsProvider provider = EnvironmentVariableCredentialsProvider.create(); if (expected != null) { - assertThat(provider.resolveCredentials()).satisfies(expected); + AwsCredentials resolvedCredentials = provider.resolveCredentials(); + assertThat(resolvedCredentials).satisfies(expected); + assertThat(resolvedCredentials.providerName()).isPresent().contains(BusinessMetricFeatureId.CREDENTIALS_ENV_VARS.value()); } else { assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class); } @@ -80,7 +85,9 @@ void configureSystemProperties_resolveCredentials(String description, configureSystemProperties(systemSettings); SystemPropertyCredentialsProvider provider = SystemPropertyCredentialsProvider.create(); if (expected != null) { - assertThat(provider.resolveCredentials()).satisfies(expected); + AwsCredentials resolvedCredentials = provider.resolveCredentials(); + assertThat(resolvedCredentials).satisfies(expected); + assertThat(resolvedCredentials.providerName()).isPresent().contains(BusinessMetricFeatureId.CREDENTIALS_JVM_SYSTEM_PROPERTIES.value()); } else { assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class); } @@ -123,6 +130,18 @@ private static List config() { ); } + @Test + void testEnvVarsClassName() { + EnvironmentVariableCredentialsProvider provider = EnvironmentVariableCredentialsProvider.create(); + Assertions.assertThat(provider.toString()).contains("EnvironmentVariableCredentialsProvider"); + } + + @Test + void testSystemPropertyClassName() { + SystemPropertyCredentialsProvider provider = SystemPropertyCredentialsProvider.create(); + Assertions.assertThat(provider.toString()).contains("SystemPropertyCredentialsProvider"); + } + private void configureEnvironmentVariables(List> systemSettings) { for (Pair setting : systemSettings) { ENVIRONMENT_VARIABLE_HELPER.set(setting.left(), setting.right()); diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingsCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingsCredentialsProviderTest.java index 81905de526ac..95cfc899460f 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingsCredentialsProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/SystemSettingsCredentialsProviderTest.java @@ -47,7 +47,7 @@ void systemPropertyCredentialsProvider_resolveCredentials_returnsCredentialsWith AwsCredentials credentials = SystemPropertyCredentialsProvider.create().resolveCredentials(); assertThat(credentials.accessKeyId()).isEqualTo("akid1"); assertThat(credentials.secretAccessKey()).isEqualTo("skid1"); - assertThat(credentials.providerName()).isPresent().contains("SystemPropertyCredentialsProvider"); + assertThat(credentials.providerName()).isPresent().contains("f"); } @Test @@ -55,6 +55,6 @@ void environmentVariableCredentialsProvider_resolveCredentials_returnsCredential AwsCredentials credentials = EnvironmentVariableCredentialsProvider.create().resolveCredentials(); assertThat(credentials.accessKeyId()).isEqualTo("akid2"); assertThat(credentials.secretAccessKey()).isEqualTo("skid2"); - assertThat(credentials.providerName()).isPresent().contains("EnvironmentVariableCredentialsProvider"); + assertThat(credentials.providerName()).isPresent().contains("g"); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java index ef1e3fb2cc9d..744b1f4a5d8d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java @@ -15,23 +15,20 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; -import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.AUTH_SOURCE; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.BUSINESS_METADATA; -import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.CONFIG_METADATA; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.SLASH; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.SPACE; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.appendSpaceAndField; -import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.uaPair; import static software.amazon.awssdk.utils.StringUtils.trim; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.ApiName; -import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; @@ -40,12 +37,10 @@ import software.amazon.awssdk.core.internal.http.HttpClientDependencies; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline; -import software.amazon.awssdk.core.internal.useragent.IdentityProviderNameMapping; import software.amazon.awssdk.core.useragent.AdditionalMetadata; import software.amazon.awssdk.core.useragent.BusinessMetricCollection; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.identity.spi.Identity; -import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.Pair; @@ -118,10 +113,6 @@ private String finalizeUserAgent(RequestExecutionContext context) { userAgentMetadata.forEach(s -> javaUserAgent.append(SPACE).append(s)); } - //add remaining SDK user agent properties - identityProviderName(context.executionAttributes()).ifPresent( - authSource -> appendSpaceAndField(javaUserAgent, CONFIG_METADATA, uaPair(AUTH_SOURCE, authSource))); - Optional businessMetrics = getBusinessMetricsString(context.executionAttributes(), groupedApiNames.right()); businessMetrics.ifPresent( metrics -> appendSpaceAndField(javaUserAgent, BUSINESS_METADATA, metrics) @@ -156,29 +147,33 @@ private static Optional getBusinessMetricsString(ExecutionAttributes exe Collection metricsFromApiNames) { BusinessMetricCollection businessMetrics = executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - if (businessMetrics == null && CollectionUtils.isNullOrEmpty(metricsFromApiNames)) { - return Optional.empty(); - } if (businessMetrics == null) { businessMetrics = new BusinessMetricCollection(); } businessMetrics.merge(metricsFromApiNames); - return Optional.of(businessMetrics.asBoundedString()); - } - private static Optional identityProviderName(ExecutionAttributes executionAttributes) { - SelectedAuthScheme selectedAuthScheme = executionAttributes - .getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME); - if (selectedAuthScheme == null) { + credentialProviderBusinessMetrics(executionAttributes).ifPresent(businessMetrics::merge); + + if (businessMetrics.recordedMetrics().isEmpty()) { return Optional.empty(); } - return providerNameFromIdentity(selectedAuthScheme); + + return Optional.of(businessMetrics.asBoundedString()); } - private static Optional providerNameFromIdentity(SelectedAuthScheme selectedAuthScheme) { - CompletableFuture identityFuture = selectedAuthScheme.identity(); - T identity = CompletableFutureUtils.joinLikeSync(identityFuture); - return identity.providerName().flatMap(IdentityProviderNameMapping::mapFrom); + private static Optional> credentialProviderBusinessMetrics( + ExecutionAttributes executionAttributes) { + return Optional.ofNullable( + executionAttributes.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME)) + .map(selectedAuthScheme -> + CompletableFutureUtils.joinLikeSync(selectedAuthScheme.identity())) + .flatMap(Identity::providerName) + .map(providerName -> { + if (StringUtils.isBlank(providerName)) { + return Collections.emptyList(); + } + return Arrays.asList(providerName.split(",")); + }); } /** 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..a28be7398698 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 @@ -36,12 +36,34 @@ public enum BusinessMetricFeatureId { S3_TRANSFER("G"), GZIP_REQUEST_COMPRESSION("L"), //TODO(metrics): Not working, compression happens after header ENDPOINT_OVERRIDE("N"), + S3_EXPRESS_BUCKET("J"), ACCOUNT_ID_MODE_PREFERRED("P"), ACCOUNT_ID_MODE_DISABLED("Q"), ACCOUNT_ID_MODE_REQUIRED("R"), RESOLVED_ACCOUNT_ID("T"), DDB_MAPPER("d"), BEARER_SERVICE_ENV_VARS("3"), + CREDENTIALS_CODE("e"), + CREDENTIALS_JVM_SYSTEM_PROPERTIES("f"), + CREDENTIALS_ENV_VARS("g"), + CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN("h"), + CREDENTIALS_STS_ASSUME_ROLE("i"), + CREDENTIALS_STS_ASSUME_ROLE_SAML("j"), + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID("k"), + CREDENTIALS_STS_FEDERATION_TOKEN("l"), + CREDENTIALS_STS_SESSION_TOKEN("m"), + CREDENTIALS_PROFILE("n"), + CREDENTIALS_PROFILE_SOURCE_PROFILE("o"), + CREDENTIALS_PROFILE_NAMED_PROVIDER("p"), + CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN("q"), + CREDENTIALS_PROFILE_SSO("r"), + CREDENTIALS_SSO("s"), + CREDENTIALS_PROFILE_SSO_LEGACY("t"), + CREDENTIALS_SSO_LEGACY("u"), + CREDENTIALS_PROFILE_PROCESS("v"), + CREDENTIALS_PROCESS("w"), + CREDENTIALS_HTTP("z"), + CREDENTIALS_IMDS("0"), UNKNOWN("Unknown"); private static final Map VALUE_MAP = diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java index d02654a78071..4db0103b7e3c 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java @@ -60,7 +60,7 @@ public class ApplyUserAgentStageTest { (HttpSigner) Mockito.mock(HttpSigner.class), AuthSchemeOption.builder().schemeId("mock").build()); - private static final String PROVIDER_SOURCE = "ProcessCredentialsProvider"; + private static final String PROVIDER_SOURCE = "w"; private static final AwsCredentialsIdentity IDENTITY_WITHOUT_SOURCE = AwsCredentialsIdentity.create("akid", "secret"); @@ -149,7 +149,7 @@ public void when_identityContainsProvider_authSourceIsPresent() throws Exception List userAgentHeaders = request.headers().get(HEADER_USER_AGENT); assertThat(userAgentHeaders).isNotNull().hasSize(1); - assertThat(userAgentHeaders.get(0)).contains("auth-source#proc"); + assertThat(userAgentHeaders.get(0)).contains("m/w"); } private static HttpClientDependencies dependencies(String clientUserAgent) { diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java index b55200c5e536..441e575687e4 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java @@ -21,6 +21,7 @@ import software.amazon.awssdk.core.SelectedAuthScheme; 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.endpoints.Endpoint; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; import software.amazon.awssdk.services.s3.endpoints.internal.KnownS3ExpressEndpointProperty; @@ -57,4 +58,15 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib } return false; } + + /** + * Adds S3 Express business metric if applicable for the current operation. + */ + public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) { + if (executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) + .ifPresent(businessMetrics -> + businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value())); + } + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java new file mode 100644 index 000000000000..4776ea0249d7 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java @@ -0,0 +1,174 @@ +/* + * 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 java.util.List; +import java.util.function.UnaryOperator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +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; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Unit test to verify that S3 Express operations include the correct business metric feature ID + * in the User-Agent header. + */ +public class S3ExpressUserAgentTest { + private static final String KEY = "test-feature-id.txt"; + private static final String CONTENTS = "test content for feature id validation"; + private static final String S3_EXPRESS_BUCKET = "my-test-bucket--use1-az4--x-s3"; + private static final String REGULAR_BUCKET = "my-test-bucket-regular"; + + public static final UnaryOperator METRIC_SEARCH_PATTERN = + metric -> ".*m/[a-zA-Z0-9+-,]*" + metric + ".*"; + + private MockSyncHttpClient mockHttpClient; + private S3Client s3Client; + + @BeforeEach + void setup() { + // Mock HTTP client + mockHttpClient = new MockSyncHttpClient(); + + // Mock CreateSession response for S3 Express authentication + String createSessionResponse = "\n" + + "\n" + + " \n" + + " mock-session-token\n" + + " mock-secret-key\n" + + " mock-access-key\n" + + " 2025-12-31T23:59:59Z\n" + + " \n" + + ""; + + HttpExecuteResponse createSessionHttpResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(createSessionResponse))) + .build(); + + HttpExecuteResponse putResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + + HttpExecuteResponse getResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(CONTENTS))) + .build(); + + mockHttpClient.stubResponses( + createSessionHttpResponse, // First CreateSession call for S3 Express bucket + putResponse, // PUT operation + createSessionHttpResponse, // Second CreateSession call for S3 Express bucket + getResponse, // GET operation + putResponse, // PUT operation for regular bucket + getResponse // GET operation for regular bucket + ); + + // S3 client with mocked HTTP client + s3Client = S3Client.builder() + .region(Region.US_EAST_1) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .httpClient(mockHttpClient) + .build(); + } + + @Test + void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(S3_EXPRESS_BUCKET) + .key(KEY) + .build(); + + s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + + assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J")); + } + + @Test + void getObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(S3_EXPRESS_BUCKET) + .key(KEY) + .build(); + + s3Client.getObject(getRequest, ResponseTransformer.toBytes()); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + + assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J")); + } + + @Test + void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(REGULAR_BUCKET) + .key(KEY) + .build(); + + s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + + assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J")); + } + + @Test + void getObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(REGULAR_BUCKET) + .key(KEY) + .build(); + + s3Client.getObject(getRequest, ResponseTransformer.toBytes()); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + + assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J")); + } + +} diff --git a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java index ce4fbaf2ca97..5002595877e6 100644 --- a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java +++ b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java @@ -25,11 +25,13 @@ import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sso.SsoClient; import software.amazon.awssdk.services.sso.internal.SessionCredentialsHolder; import software.amazon.awssdk.services.sso.model.GetRoleCredentialsRequest; import software.amazon.awssdk.services.sso.model.RoleCredentials; import software.amazon.awssdk.utils.SdkAutoCloseable; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; import software.amazon.awssdk.utils.cache.CachedSupplier; @@ -51,7 +53,7 @@ @SdkPublicApi public final class SsoCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable, ToCopyableBuilder { - private static final String PROVIDER_NAME = "SsoCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_SSO.value(); private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1); private static final Duration DEFAULT_PREFETCH_TIME = Duration.ofMinutes(5); @@ -59,6 +61,7 @@ public final class SsoCredentialsProvider implements AwsCredentialsProvider, Sdk private static final String ASYNC_THREAD_NAME = "sdk-sso-credentials-provider"; private final Supplier getRoleCredentialsRequestSupplier; + private final String source; private final SsoClient ssoClient; private final Duration staleTime; @@ -77,6 +80,7 @@ private SsoCredentialsProvider(BuilderImpl builder) { this.staleTime = Optional.ofNullable(builder.staleTime).orElse(DEFAULT_STALE_TIME); this.prefetchTime = Optional.ofNullable(builder.prefetchTime).orElse(DEFAULT_PREFETCH_TIME); + this.source = builder.source; this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; CachedSupplier.Builder cacheBuilder = @@ -95,11 +99,11 @@ private SsoCredentialsProvider(BuilderImpl builder) { */ private RefreshResult updateSsoCredentials() { SessionCredentialsHolder credentials = getUpdatedCredentials(ssoClient); - Instant acutalTokenExpiration = credentials.sessionCredentialsExpiration(); + Instant actualTokenExpiration = credentials.sessionCredentialsExpiration(); return RefreshResult.builder(credentials) - .staleTime(acutalTokenExpiration.minus(staleTime)) - .prefetchTime(acutalTokenExpiration.minus(prefetchTime)) + .staleTime(actualTokenExpiration.minus(staleTime)) + .prefetchTime(actualTokenExpiration.minus(prefetchTime)) .build(); } @@ -112,11 +116,19 @@ private SessionCredentialsHolder getUpdatedCredentials(SsoClient ssoClient) { .secretAccessKey(roleCredentials.secretAccessKey()) .sessionToken(roleCredentials.sessionToken()) .accountId(request.accountId()) - .providerName(PROVIDER_NAME) + .providerName(providerName()) .build(); return new SessionCredentialsHolder(sessionCredentials, Instant.ofEpochMilli(roleCredentials.expiration())); } + private String providerName() { + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; + } + /** * The amount of time, relative to session token expiration, that the cached credentials are considered stale and * should no longer be used. All threads will block until the value is updated. @@ -206,6 +218,12 @@ public interface Builder extends CopyableBuilder getRoleCredentialsRequestSupplier); + /** + * An optional string list of {@link software.amazon.awssdk.core.useragent.BusinessMetricFeatureId} denoting previous + * credentials providers that are chained with this one. + */ + Builder source(String source); + /** * Create a {@link SsoCredentialsProvider} using the configuration applied to this builder. * @return @@ -220,6 +238,7 @@ protected static final class BuilderImpl implements Builder { private Duration staleTime; private Duration prefetchTime; private Supplier getRoleCredentialsRequestSupplier; + private String source; BuilderImpl() { @@ -231,6 +250,7 @@ public BuilderImpl(SsoCredentialsProvider provider) { this.staleTime = provider.staleTime; this.prefetchTime = provider.prefetchTime; this.getRoleCredentialsRequestSupplier = provider.getRoleCredentialsRequestSupplier; + this.source = provider.source; } @Override @@ -268,6 +288,12 @@ public Builder refreshRequest(Supplier getRoleCredent return this; } + @Override + public Builder source(String source) { + this.source = source; + return this; + } + @Override public SsoCredentialsProvider build() { return new SsoCredentialsProvider(this); diff --git a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactory.java b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactory.java index f3b910c3e1fa..efa8379315ae 100644 --- a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactory.java +++ b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactory.java @@ -63,10 +63,7 @@ public class SsoProfileCredentialsProviderFactory implements ProfileCredentialsP */ @Override public AwsCredentialsProvider create(ProfileProviderCredentialsContext credentialsContext) { - return new SsoProfileCredentialsProvider(credentialsContext.profile(), - credentialsContext.profileFile(), - sdkTokenProvider(credentialsContext.profile(), - credentialsContext.profileFile())); + return new SsoProfileCredentialsProvider(credentialsContext, sdkTokenProvider(credentialsContext)); } /** @@ -74,26 +71,27 @@ public AwsCredentialsProvider create(ProfileProviderCredentialsContext credentia * This method is only used for testing. */ @SdkTestInternalApi - public AwsCredentialsProvider create(Profile profile, ProfileFile profileFile, + public AwsCredentialsProvider create(ProfileProviderCredentialsContext credentialsContext, SdkTokenProvider tokenProvider) { - return new SsoProfileCredentialsProvider(profile, profileFile, tokenProvider); + return new SsoProfileCredentialsProvider(credentialsContext, tokenProvider); } /** * A wrapper for a {@link SsoCredentialsProvider} that is returned by this factory when {@link - * #create(ProfileProviderCredentialsContext)} * or {@link #create(Profile, ProfileFile, SdkTokenProvider)} is invoked. This - * wrapper is important because it ensures * the parent credentials provider is closed when the sso credentials provider is no - * longer needed. + * #create(ProfileProviderCredentialsContext)} * or {@link #create(ProfileProviderCredentialsContext, SdkTokenProvider)} + * is invoked. This wrapper is important because it ensures * the parent credentials provider is closed when the sso + * credentials provider is no longer needed. */ private static final class SsoProfileCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { private final SsoClient ssoClient; private final SsoCredentialsProvider credentialsProvider; - private SsoProfileCredentialsProvider(Profile profile, ProfileFile profileFile, + private SsoProfileCredentialsProvider(ProfileProviderCredentialsContext credentialsContext, SdkTokenProvider tokenProvider) { + Profile profile = credentialsContext.profile(); String ssoAccountId = profile.properties().get(ProfileProperty.SSO_ACCOUNT_ID); String ssoRoleName = profile.properties().get(ProfileProperty.SSO_ROLE_NAME); - String ssoRegion = regionFromProfileOrSession(profile, profileFile); + String ssoRegion = regionFromProfileOrSession(profile, credentialsContext.profileFile()); this.ssoClient = SsoClient.builder() .credentialsProvider(AnonymousCredentialsProvider.create()) @@ -114,6 +112,7 @@ private SsoProfileCredentialsProvider(Profile profile, ProfileFile profileFile, this.credentialsProvider = SsoCredentialsProvider.builder() .ssoClient(ssoClient) .refreshRequest(supplier) + .source(credentialsContext.source()) .build(); } @@ -157,7 +156,9 @@ private static Profile ssoSessionInProfile(String sessionName, ProfileFile profi return ssoProfile; } - private static SdkTokenProvider sdkTokenProvider(Profile profile, ProfileFile profileFile) { + private static SdkTokenProvider sdkTokenProvider(ProfileProviderCredentialsContext credentialsContext) { + Profile profile = credentialsContext.profile(); + ProfileFile profileFile = credentialsContext.profileFile(); Optional ssoSession = profile.property(ProfileSection.SSO_SESSION.getPropertyKeyName()); @@ -172,11 +173,9 @@ private static SdkTokenProvider sdkTokenProvider(Profile profile, ProfileFile pr .profileFile(() -> profileFile) .profileName(profile.name()) .build()); - } else { - return new SsoAccessTokenProvider(generateCachedTokenPath( - profile.properties().get(ProfileProperty.SSO_START_URL), TOKEN_DIRECTORY)); - } + return new SsoAccessTokenProvider(generateCachedTokenPath(profile.properties().get(ProfileProperty.SSO_START_URL), + TOKEN_DIRECTORY)); } private static void validateCommonProfileProperties(Profile profile, Profile ssoSessionProfileFile, String propertyName) { diff --git a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java index 9540a77ba6c6..d7be6cdd852c 100644 --- a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java +++ b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sso.SsoClient; import software.amazon.awssdk.services.sso.model.GetRoleCredentialsRequest; import software.amazon.awssdk.services.sso.model.GetRoleCredentialsResponse; @@ -136,7 +137,7 @@ private void callClientWithCredentialsProvider(Instant credentialsExpirationDate assertThat(actualCredentials.accessKeyId()).isEqualTo("a"); assertThat(actualCredentials.secretAccessKey()).isEqualTo("b"); assertThat(actualCredentials.sessionToken()).isEqualTo("c"); - assertThat(actualCredentials.providerName()).isPresent().contains("SsoCredentialsProvider"); + assertThat(actualCredentials.providerName()).isPresent().contains(BusinessMetricFeatureId.CREDENTIALS_SSO.value()); assertThat(actualCredentials.accountId()).isPresent().contains("123456789"); } } diff --git a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java index c5cb2b57834d..8da326bf589f 100644 --- a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java +++ b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java @@ -79,9 +79,12 @@ public void createSsoCredentialsProviderWithFactorySucceed() throws IOException cachedTokenFilePath); SsoProfileCredentialsProviderFactory factory = new SsoProfileCredentialsProviderFactory(); - assertThat(factory.create(profileFile.profile("foo").get(), - profileFile, - tokenProvider)).isInstanceOf(AwsCredentialsProvider.class); + assertThat(factory.create(ProfileProviderCredentialsContext.builder() + .profile(profileFile.profile("foo").get()) + .profileFile(profileFile) + .build(), + tokenProvider)) + .isInstanceOf(AwsCredentialsProvider.class); } private Path prepareTestCachedTokenFile(String tokenFileContent, String generatedTokenFileName) throws IOException { @@ -169,7 +172,10 @@ public void tokenResolvedFromTokenProvider(@Mock SdkTokenProvider sdkTokenProvid "sso_start_url=https//d-abc123.awsapps.com/start"); SsoProfileCredentialsProviderFactory factory = new SsoProfileCredentialsProviderFactory(); when(sdkTokenProvider.resolveToken()).thenReturn(SsoAccessToken.builder().accessToken("sample").expiresAt(Instant.now()).build()); - AwsCredentialsProvider credentialsProvider = factory.create(profileFile.profile("test").get(), profileFile, sdkTokenProvider); + AwsCredentialsProvider credentialsProvider = factory.create(ProfileProviderCredentialsContext.builder() + .profile(profileFile.profile("test").get()) + .profileFile(profileFile) + .build(), sdkTokenProvider); try { credentialsProvider.resolveCredentials(); } catch (Exception e) { diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProvider.java index a59570be0103..da7c831d1a26 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProvider.java @@ -25,9 +25,11 @@ import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -49,8 +51,9 @@ public final class StsAssumeRoleCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsAssumeRoleCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE.value(); private final Supplier assumeRoleRequestSupplier; + private final String source; /** * @see #builder() @@ -60,6 +63,7 @@ private StsAssumeRoleCredentialsProvider(Builder builder) { Validate.notNull(builder.assumeRoleRequestSupplier, "Assume role request must not be null."); this.assumeRoleRequestSupplier = builder.assumeRoleRequestSupplier; + this.source = builder.source; } /** @@ -75,7 +79,7 @@ protected AwsSessionCredentials getUpdatedCredentials(StsClient stsClient) { Validate.notNull(assumeRoleRequest, "Assume role request must not be null."); AssumeRoleResponse assumeRoleResponse = stsClient.assumeRole(assumeRoleRequest); return fromStsCredentials(assumeRoleResponse.credentials(), - PROVIDER_NAME, + providerName(), accountIdFromArn(assumeRoleResponse.assumedRoleUser())); } @@ -93,7 +97,11 @@ public Builder toBuilder() { @Override String providerName() { - return PROVIDER_NAME; + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; } /** @@ -103,6 +111,7 @@ String providerName() { @NotThreadSafe public static final class Builder extends BaseBuilder { private Supplier assumeRoleRequestSupplier; + private String source; private Builder() { super(StsAssumeRoleCredentialsProvider::new); @@ -111,6 +120,7 @@ private Builder() { private Builder(StsAssumeRoleCredentialsProvider provider) { super(StsAssumeRoleCredentialsProvider::new, provider); this.assumeRoleRequestSupplier = provider.assumeRoleRequestSupplier; + this.source = provider.source; } /** @@ -145,6 +155,15 @@ public Builder refreshRequest(Consumer assumeRoleRequ return refreshRequest(AssumeRoleRequest.builder().applyMutation(assumeRoleRequest).build()); } + /** + * An optional string list of {@link BusinessMetricFeatureId} denoting previous credentials providers + * that are chained with this one. + */ + public Builder source(String source) { + this.source = source; + return this; + } + @Override public StsAssumeRoleCredentialsProvider build() { return super.build(); diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProvider.java index 6d99b555e311..a66c55104285 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProvider.java @@ -25,9 +25,11 @@ import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleWithSamlRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleWithSamlResponse; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -48,8 +50,9 @@ public final class StsAssumeRoleWithSamlCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsAssumeRoleWithSamlCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE_SAML.value(); private final Supplier assumeRoleWithSamlRequestSupplier; + private final String source; /** @@ -60,6 +63,7 @@ private StsAssumeRoleWithSamlCredentialsProvider(Builder builder) { Validate.notNull(builder.assumeRoleWithSamlRequestSupplier, "Assume role with SAML request must not be null."); this.assumeRoleWithSamlRequestSupplier = builder.assumeRoleWithSamlRequestSupplier; + this.source = builder.source; } /** @@ -75,7 +79,7 @@ protected AwsSessionCredentials getUpdatedCredentials(StsClient stsClient) { Validate.notNull(assumeRoleWithSamlRequest, "Assume role with saml request must not be null."); AssumeRoleWithSamlResponse assumeRoleResponse = stsClient.assumeRoleWithSAML(assumeRoleWithSamlRequest); return fromStsCredentials(assumeRoleResponse.credentials(), - PROVIDER_NAME, + providerName(), accountIdFromArn(assumeRoleResponse.assumedRoleUser())); } @@ -86,7 +90,11 @@ public Builder toBuilder() { @Override String providerName() { - return PROVIDER_NAME; + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; } /** @@ -96,6 +104,7 @@ String providerName() { @NotThreadSafe public static final class Builder extends BaseBuilder { private Supplier assumeRoleWithSamlRequestSupplier; + private String source; private Builder() { super(StsAssumeRoleWithSamlCredentialsProvider::new); @@ -104,6 +113,7 @@ private Builder() { public Builder(StsAssumeRoleWithSamlCredentialsProvider provider) { super(StsAssumeRoleWithSamlCredentialsProvider::new, provider); this.assumeRoleWithSamlRequestSupplier = provider.assumeRoleWithSamlRequestSupplier; + this.source = provider.source; } /** @@ -138,6 +148,18 @@ public Builder refreshRequest(Consumer assume return refreshRequest(AssumeRoleWithSamlRequest.builder().applyMutation(assumeRoleWithSamlRequest).build()); } + /** + * Configure the source of this credentials provider. This is used for business metrics tracking + * to identify the credential provider chain. + * + * @param source The source identifier for business metrics tracking. + * @return This object for chained calls. + */ + public Builder source(String source) { + this.source = source; + return this; + } + @Override public StsAssumeRoleWithSamlCredentialsProvider build() { return super.build(); diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProvider.java index 4cbb325f7458..afdf45b55ff5 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProvider.java @@ -26,9 +26,11 @@ import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; /** @@ -49,8 +51,9 @@ public final class StsAssumeRoleWithWebIdentityCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsAssumeRoleWithWebIdentityCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE_WEB_ID.value(); private final Supplier assumeRoleWithWebIdentityRequest; + private final String source; /** * @see #builder() @@ -60,6 +63,7 @@ private StsAssumeRoleWithWebIdentityCredentialsProvider(Builder builder) { notNull(builder.assumeRoleWithWebIdentityRequestSupplier, "Assume role with web identity request must not be null."); this.assumeRoleWithWebIdentityRequest = builder.assumeRoleWithWebIdentityRequestSupplier; + this.source = builder.source; } /** @@ -75,7 +79,7 @@ protected AwsSessionCredentials getUpdatedCredentials(StsClient stsClient) { notNull(request, "AssumeRoleWithWebIdentityRequest can't be null"); AssumeRoleWithWebIdentityResponse assumeRoleResponse = stsClient.assumeRoleWithWebIdentity(request); return fromStsCredentials(assumeRoleResponse.credentials(), - PROVIDER_NAME, + providerName(), accountIdFromArn(assumeRoleResponse.assumedRoleUser())); } @@ -86,7 +90,11 @@ public Builder toBuilder() { @Override String providerName() { - return PROVIDER_NAME; + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; } /** @@ -96,6 +104,7 @@ String providerName() { @NotThreadSafe public static final class Builder extends BaseBuilder { private Supplier assumeRoleWithWebIdentityRequestSupplier; + private String source; private Builder() { super(StsAssumeRoleWithWebIdentityCredentialsProvider::new); @@ -104,6 +113,7 @@ private Builder() { public Builder(StsAssumeRoleWithWebIdentityCredentialsProvider provider) { super(StsAssumeRoleWithWebIdentityCredentialsProvider::new, provider); this.assumeRoleWithWebIdentityRequestSupplier = provider.assumeRoleWithWebIdentityRequest; + this.source = provider.source; } /** @@ -139,6 +149,18 @@ public Builder refreshRequest(Consumer .build()); } + /** + * Configure the source of this credentials provider. This is used for business metrics tracking + * to identify the credential provider chain. + * + * @param source The source identifier for business metrics tracking. + * @return This object for chained calls. + */ + public Builder source(String source) { + this.source = source; + return this; + } + @Override public StsAssumeRoleWithWebIdentityCredentialsProvider build() { return super.build(); diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProvider.java index da28815b686e..18bc9149c7fe 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProvider.java @@ -23,11 +23,13 @@ import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.endpoints.internal.Arn; import software.amazon.awssdk.services.sts.model.FederatedUser; import software.amazon.awssdk.services.sts.model.GetFederationTokenRequest; import software.amazon.awssdk.services.sts.model.GetFederationTokenResponse; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -48,9 +50,10 @@ public class StsGetFederationTokenCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsGetFederationTokenCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_STS_FEDERATION_TOKEN.value(); private final GetFederationTokenRequest getFederationTokenRequest; + private final String source; /** * @see #builder() @@ -60,6 +63,7 @@ private StsGetFederationTokenCredentialsProvider(Builder builder) { Validate.notNull(builder.getFederationTokenRequest, "Get session token request must not be null."); this.getFederationTokenRequest = builder.getFederationTokenRequest; + this.source = builder.source; } /** @@ -73,7 +77,7 @@ public static Builder builder() { protected AwsSessionCredentials getUpdatedCredentials(StsClient stsClient) { GetFederationTokenResponse federationToken = stsClient.getFederationToken(getFederationTokenRequest); return fromStsCredentials(federationToken.credentials(), - PROVIDER_NAME, + providerName(), accountIdFromArn(federationToken.federatedUser())); } @@ -93,7 +97,11 @@ public Builder toBuilder() { @Override String providerName() { - return PROVIDER_NAME; + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; } /** @@ -103,6 +111,7 @@ String providerName() { @NotThreadSafe public static final class Builder extends BaseBuilder { private GetFederationTokenRequest getFederationTokenRequest; + private String source; private Builder() { super(StsGetFederationTokenCredentialsProvider::new); @@ -111,6 +120,7 @@ private Builder() { public Builder(StsGetFederationTokenCredentialsProvider provider) { super(StsGetFederationTokenCredentialsProvider::new, provider); this.getFederationTokenRequest = provider.getFederationTokenRequest; + this.source = provider.source; } /** @@ -134,6 +144,18 @@ public Builder refreshRequest(Consumer getFed return refreshRequest(GetFederationTokenRequest.builder().applyMutation(getFederationTokenRequest).build()); } + /** + * Configure the source of this credentials provider. This is used for business metrics tracking + * to identify the credential provider chain. + * + * @param source The source identifier for business metrics tracking. + * @return This object for chained calls. + */ + public Builder source(String source) { + this.source = source; + return this; + } + @Override public StsGetFederationTokenCredentialsProvider build() { return super.build(); diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProvider.java index 8ca66114d2be..936b141b1d08 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProvider.java @@ -23,9 +23,11 @@ import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.GetSessionTokenRequest; import software.amazon.awssdk.services.sts.model.GetSessionTokenResponse; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -46,9 +48,10 @@ public class StsGetSessionTokenCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsGetSessionTokenCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_STS_SESSION_TOKEN.value(); private final GetSessionTokenRequest getSessionTokenRequest; + private final String source; /** * @see #builder() @@ -58,6 +61,7 @@ private StsGetSessionTokenCredentialsProvider(Builder builder) { Validate.notNull(builder.getSessionTokenRequest, "Get session token request must not be null."); this.getSessionTokenRequest = builder.getSessionTokenRequest; + this.source = builder.source; } /** @@ -70,7 +74,7 @@ public static Builder builder() { @Override protected AwsSessionCredentials getUpdatedCredentials(StsClient stsClient) { GetSessionTokenResponse sessionToken = stsClient.getSessionToken(getSessionTokenRequest); - return fromStsCredentials(sessionToken.credentials(), PROVIDER_NAME); + return fromStsCredentials(sessionToken.credentials(), providerName()); } @Override @@ -80,7 +84,11 @@ public Builder toBuilder() { @Override String providerName() { - return PROVIDER_NAME; + String providerName = PROVIDER_NAME; + if (!StringUtils.isEmpty(this.source)) { + providerName = String.format("%s,%s", this.source, providerName); + } + return providerName; } /** @@ -90,6 +98,7 @@ String providerName() { @NotThreadSafe public static final class Builder extends BaseBuilder { private GetSessionTokenRequest getSessionTokenRequest = GetSessionTokenRequest.builder().build(); + private String source; private Builder() { super(StsGetSessionTokenCredentialsProvider::new); @@ -98,6 +107,7 @@ private Builder() { public Builder(StsGetSessionTokenCredentialsProvider provider) { super(StsGetSessionTokenCredentialsProvider::new, provider); this.getSessionTokenRequest = provider.getSessionTokenRequest; + this.source = provider.source; } /** @@ -122,6 +132,18 @@ public Builder refreshRequest(GetSessionTokenRequest getSessionTokenRequest) { public Builder refreshRequest(Consumer getFederationTokenRequest) { return refreshRequest(GetSessionTokenRequest.builder().applyMutation(getFederationTokenRequest).build()); } + + /** + * Configure the source of this credentials provider. This is used for business metrics tracking + * to identify the credential provider chain. + * + * @param source The source identifier for business metrics tracking. + * @return This object for chained calls. + */ + public Builder source(String source) { + this.source = source; + return this; + } @Override public StsGetSessionTokenCredentialsProvider build() { diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java index c812da56e21e..0a8ee365951b 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -30,6 +31,7 @@ import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.auth.credentials.internal.WebIdentityTokenCredentialProperties; import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.internal.AssumeRoleWithWebIdentityRequestSupplier; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; @@ -56,7 +58,7 @@ public final class StsWebIdentityTokenFileCredentialsProvider extends StsCredentialsProvider implements ToCopyableBuilder { - private static final String PROVIDER_NAME = "StsWebIdentityTokenFileCredentialsProvider"; + private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN.value(); private final AwsCredentialsProvider credentialsProvider; private final RuntimeException loadException; @@ -132,7 +134,16 @@ public AwsCredentials resolveCredentials() { if (loadException != null) { throw loadException; } - return credentialsProvider.resolveCredentials(); + AwsCredentials awsCredentials = credentialsProvider.resolveCredentials(); + if (awsCredentials instanceof AwsSessionCredentials) { + AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) awsCredentials; + Optional providerName = awsCredentials.providerName(); + if (providerName.isPresent()) { + return sessionCredentials.copy(s -> s.providerName(providerName.get() + "," + PROVIDER_NAME)); + } + return sessionCredentials; + } + return awsCredentials; } @Override @@ -303,4 +314,4 @@ public StsWebIdentityTokenFileCredentialsProvider build() { } } -} \ No newline at end of file +} diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/AssumeRoleWithWebIdentityRequestSupplier.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/AssumeRoleWithWebIdentityRequestSupplier.java index 03b91890af8a..a19e38120782 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/AssumeRoleWithWebIdentityRequestSupplier.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/AssumeRoleWithWebIdentityRequestSupplier.java @@ -31,12 +31,13 @@ public class AssumeRoleWithWebIdentityRequestSupplier implements Supplier "aws-sdk-java-" + System.currentTimeMillis()); @@ -76,6 +76,7 @@ private StsProfileCredentialsProvider(AwsCredentialsProvider parentCredentialsPr this.credentialsProvider = StsAssumeRoleCredentialsProvider.builder() .stsClient(stsClient) .refreshRequest(assumeRoleRequest) + .source(source) .build(); } diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/StsWebIdentityCredentialsProviderFactory.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/StsWebIdentityCredentialsProviderFactory.java index 86340d4f857d..508415676a64 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/StsWebIdentityCredentialsProviderFactory.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/internal/StsWebIdentityCredentialsProviderFactory.java @@ -87,6 +87,7 @@ private StsWebIdentityCredentialsProvider(WebIdentityTokenCredentialProperties c AssumeRoleWithWebIdentityRequestSupplier.builder() .assumeRoleWithWebIdentityRequest(requestBuilder.build()) .webIdentityTokenFile(credentialProperties.webIdentityTokenFile()) + .source(credentialProperties.source()) .build(); StsAssumeRoleWithWebIdentityCredentialsProvider.Builder builder = diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProviderTest.java index e4d7b6c6bc5c..b36cd6e67613 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleCredentialsProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.sts.auth; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; @@ -51,6 +52,6 @@ protected AssumeRoleResponse callClient(StsClient client, AssumeRoleRequest requ @Override protected String providerName() { - return "StsAssumeRoleCredentialsProvider"; + return BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE.value(); } } diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProviderTest.java index fb4729f98f79..34c503ac37da 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithSamlCredentialsProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.sts.auth; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithSamlCredentialsProvider.Builder; import software.amazon.awssdk.services.sts.model.AssumeRoleWithSamlRequest; @@ -54,6 +55,6 @@ protected AssumeRoleWithSamlResponse callClient(StsClient client, AssumeRoleWith @Override protected String providerName() { - return "StsAssumeRoleWithSamlCredentialsProvider"; + return BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE_SAML.value(); } } diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProviderTest.java index d037597897a2..8f1e1c4808c3 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsAssumeRoleWithWebIdentityCredentialsProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.sts.auth; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithWebIdentityCredentialsProvider.Builder; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; @@ -53,6 +54,6 @@ protected AssumeRoleWithWebIdentityResponse callClient(StsClient client, AssumeR @Override protected String providerName() { - return "StsAssumeRoleWithWebIdentityCredentialsProvider"; + return BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE_WEB_ID.value(); } } diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProviderTest.java index bdc50a817aaa..b5154f646ff6 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetFederationTokenCredentialsProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.sts.auth; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsGetFederationTokenCredentialsProvider.Builder; import software.amazon.awssdk.services.sts.model.AssumedRoleUser; @@ -54,6 +55,6 @@ protected GetFederationTokenResponse callClient(StsClient client, GetFederationT @Override protected String providerName() { - return "StsGetFederationTokenCredentialsProvider"; + return BusinessMetricFeatureId.CREDENTIALS_STS_FEDERATION_TOKEN.value(); } } diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProviderTest.java index 18f9feadf796..1ab263152602 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsGetSessionTokenCredentialsProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.sts.auth; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsGetSessionTokenCredentialsProvider.Builder; import software.amazon.awssdk.services.sts.model.AssumedRoleUser; @@ -52,6 +53,6 @@ protected GetSessionTokenResponse callClient(StsClient client, GetSessionTokenRe @Override protected String providerName() { - return "StsGetSessionTokenCredentialsProvider"; + return BusinessMetricFeatureId.CREDENTIALS_STS_SESSION_TOKEN.value(); } } diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialsProviderBaseTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialsProviderBaseTest.java index cb3ca75140bf..7d64f194edde 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialsProviderBaseTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialsProviderBaseTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsWebIdentityTokenFileCredentialsProvider.Builder; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; @@ -83,7 +84,8 @@ protected AssumeRoleWithWebIdentityResponse callClient(StsClient client, AssumeR @Override protected String providerName() { - return "StsAssumeRoleWithWebIdentityCredentialsProvider"; + return String.format("%s,%s", BusinessMetricFeatureId.CREDENTIALS_STS_ASSUME_ROLE_WEB_ID, + BusinessMetricFeatureId.CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN.value()); } private String getToken(Path file) { diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ContainerCredentialsProviderUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ContainerCredentialsProviderUserAgentTest.java new file mode 100644 index 000000000000..f64591fefd5b --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ContainerCredentialsProviderUserAgentTest.java @@ -0,0 +1,204 @@ +/* + * 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.auth.source; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.DateUtils; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ContainerCredentialsProvider correctly includes + * business metrics in the User-Agent header. This test focuses specifically on the + * CREDENTIALS_HTTP ("z") business metric feature ID. + */ +class ContainerCredentialsProviderUserAgentTest { + private static final String CONTAINER_CREDENTIALS_PATH = "/v2/credentials/test-role-arn"; + private static final String CONTAINER_SERVICE_ENDPOINT = "http://localhost:"; + + private MockSyncHttpClient mockHttpClient; + + @RegisterExtension + static WireMockExtension wireMockServer = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .configureStaticDsl(true) + .build(); + + @BeforeEach + public void setup() { + + System.setProperty(SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.property(), + CONTAINER_SERVICE_ENDPOINT + wireMockServer.getPort()); + System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.property(), + CONTAINER_CREDENTIALS_PATH); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + + stubContainerCredentialsResponses(); + } + + @AfterAll + public static void teardown() { + System.clearProperty(SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.property()); + System.clearProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.property()); + System.clearProperty(SdkSystemSetting.AWS_CONTAINER_AUTHORIZATION_TOKEN.property()); + } + + private static HttpExecuteResponse mockStsResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + private void stubContainerCredentialsResponses() { + String credentialsResponse = createCredentialsResponse("ACCESS_KEY_ID", "SECRET_ACCESS_KEY", null); + wireMockServer.stubFor(get(urlPathEqualTo(CONTAINER_CREDENTIALS_PATH)) + .willReturn(aResponse().withBody(credentialsResponse))); + } + + private void stubContainerCredentialsResponsesWithSessionToken() { + String credentialsResponse = createCredentialsResponse("ACCESS_KEY_ID", "SECRET_ACCESS_KEY", "SESSION_TOKEN"); + wireMockServer.stubFor(get(urlPathEqualTo(CONTAINER_CREDENTIALS_PATH)) + .willReturn(aResponse().withBody(credentialsResponse))); + } + + private void stubContainerCredentialsResponsesWithAuthToken() { + System.setProperty(SdkSystemSetting.AWS_CONTAINER_AUTHORIZATION_TOKEN.property(), "test-auth-token"); + + String credentialsResponse = createCredentialsResponse("ACCESS_KEY_ID", "SECRET_ACCESS_KEY", null); + wireMockServer.stubFor(get(urlPathEqualTo(CONTAINER_CREDENTIALS_PATH)) + .willReturn(aResponse().withBody(credentialsResponse))); + } + + private String createCredentialsResponse(String accessKeyId, String secretAccessKey, String sessionToken) { + StringBuilder response = new StringBuilder(); + response.append("{"); + response.append("\"AccessKeyId\":\"").append(accessKeyId).append("\","); + response.append("\"SecretAccessKey\":\"").append(secretAccessKey).append("\","); + if (sessionToken != null) { + response.append("\"Token\":\"").append(sessionToken).append("\","); + } + response.append("\"Expiration\":\"").append(DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofHours(1)))).append("\""); + response.append("}"); + return response.toString(); + } + + @ParameterizedTest + @MethodSource("containerCredentialProviders") + void userAgentString_containsContainerBusinessMetric_WhenUsingContainerCredentials( + IdentityProvider provider, String expected) throws Exception { + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream containerCredentialProviders() { + return Stream.of( + Arguments.of(ContainerCredentialsProvider.create(), "m/D,z"), + + Arguments.of(ContainerCredentialsProvider.builder() + .endpoint(CONTAINER_SERVICE_ENDPOINT + wireMockServer.getPort()) + .build(), "m/D,z") + ); + } + + @ParameterizedTest + @MethodSource("containerCredentialProvidersWithSessionToken") + void userAgentString_containsContainerBusinessMetric_WhenUsingContainerCredentialsWithSessionToken( + IdentityProvider provider, String expected) throws Exception { + + stubContainerCredentialsResponsesWithSessionToken(); + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream containerCredentialProvidersWithSessionToken() { + return Stream.of( + Arguments.of(ContainerCredentialsProvider.create(), "m/D,z") + ); + } + + @ParameterizedTest + @MethodSource("containerCredentialProvidersWithAuthToken") + void userAgentString_containsContainerBusinessMetric_WhenUsingContainerCredentialsWithAuthToken( + IdentityProvider provider, String expected) throws Exception { + + stubContainerCredentialsResponsesWithAuthToken(); + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream containerCredentialProvidersWithAuthToken() { + return Stream.of( + Arguments.of(ContainerCredentialsProvider.create(), "m/D,z") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/EnvironmentVariableCredentialsProviderUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/EnvironmentVariableCredentialsProviderUserAgentTest.java new file mode 100644 index 000000000000..b026d1e3b43d --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/EnvironmentVariableCredentialsProviderUserAgentTest.java @@ -0,0 +1,130 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that EnvironmentVariableCredentialsProvider correctly includes + * business metrics in the User-Agent header. This test focuses specifically on the + * CREDENTIALS_ENV_VARS ("g") business metric feature ID. + */ +class EnvironmentVariableCredentialsProviderUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + + @BeforeEach + public void setup() { + + // Configure environment variable credentials + System.setProperty(SdkSystemSetting.AWS_ACCESS_KEY_ID.property(), "test-access-key"); + System.setProperty(SdkSystemSetting.AWS_SECRET_ACCESS_KEY.property(), "test-secret-key"); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + } + + @AfterAll + public static void teardown() { + System.clearProperty(SdkSystemSetting.AWS_ACCESS_KEY_ID.property()); + System.clearProperty(SdkSystemSetting.AWS_SECRET_ACCESS_KEY.property()); + System.clearProperty(SdkSystemSetting.AWS_SESSION_TOKEN.property()); + } + + private static HttpExecuteResponse mockStsResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + @ParameterizedTest + @MethodSource("environmentVariableCredentialProviders") + void userAgentString_containsEnvironmentVariableBusinessMetric_WhenUsingEnvironmentVariableCredentials( + IdentityProvider provider, String expected) throws Exception { + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } catch (Exception e) { + } + } + + private static Stream environmentVariableCredentialProviders() { + return Stream.of( + Arguments.of(EnvironmentVariableCredentialsProvider.create(), "m/D,g") + ); + } + + @ParameterizedTest + @MethodSource("environmentVariableCredentialProvidersWithSessionToken") + void userAgentString_containsEnvironmentVariableBusinessMetric_WhenUsingEnvironmentVariableCredentialsWithSessionToken( + IdentityProvider provider, String expected) throws Exception { + + System.setProperty(SdkSystemSetting.AWS_SESSION_TOKEN.property(), "test-session-token"); + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } catch (Exception e) { + } + } + + private static Stream environmentVariableCredentialProvidersWithSessionToken() { + return Stream.of( + Arguments.of(EnvironmentVariableCredentialsProvider.create(), "m/D,g") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ImdsUserAgentProviderTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ImdsUserAgentProviderTest.java new file mode 100644 index 000000000000..07bf771d62c9 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ImdsUserAgentProviderTest.java @@ -0,0 +1,186 @@ +/* + * 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.auth.source; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.DateUtils; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that InstanceProfileCredentialsProvider (IMDS) correctly includes + * business metrics in the User-Agent header. + */ +class ImdsUserAgentProviderTest { + private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; + private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/"; + private static final String TEST_ROLE_NAME = "test-role"; + private static final String TOKEN_STUB = "test-token"; + + private MockSyncHttpClient mockHttpClient; + + @RegisterExtension + static WireMockExtension wireMockServer = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .configureStaticDsl(true) + .build(); + + @BeforeEach + public void setup() { + + System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), + "http://localhost:" + wireMockServer.getPort()); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + + stubImdsResponses(); + } + + @AfterAll + public static void teardown() { + System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property()); + } + + private static HttpExecuteResponse mockStsResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + private void stubImdsResponses() { + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TEST_ROLE_NAME))); + + String credentialsResponse = createCredentialsResponse("ACCESS_KEY_ID", "SECRET_ACCESS_KEY", + null); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + TEST_ROLE_NAME)) + .willReturn(aResponse().withBody(credentialsResponse))); + } + + private void stubImdsResponsesWithSessionToken() { + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TEST_ROLE_NAME))); + + String credentialsResponse = createCredentialsResponse("ACCESS_KEY_ID", "SECRET_ACCESS_KEY", + "SESSION_TOKEN"); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + TEST_ROLE_NAME)) + .willReturn(aResponse().withBody(credentialsResponse))); + } + + private String createCredentialsResponse(String accessKeyId, String secretAccessKey, String sessionToken) { + StringBuilder response = new StringBuilder(); + response.append("{"); + response.append("\"AccessKeyId\":\"").append(accessKeyId).append("\","); + response.append("\"SecretAccessKey\":\"").append(secretAccessKey).append("\","); + if (sessionToken != null) { + response.append("\"Token\":\"").append(sessionToken).append("\","); + } + response.append("\"Expiration\":\"").append(DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofHours(1)))) + .append("\""); + response.append("}"); + return response.toString(); + } + + @ParameterizedTest + @MethodSource("imdsCredentialProviders") + void userAgentString_containsImdsBusinessMetric_WhenUsingInstanceProfileCredentials( + IdentityProvider provider, String expected) throws Exception { + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream imdsCredentialProviders() { + return Stream.of( + Arguments.of(InstanceProfileCredentialsProvider.create(), "m/D,0"), + + Arguments.of(InstanceProfileCredentialsProvider.builder() + .endpoint("http://localhost:" + wireMockServer.getPort()) + .build(), "m/D,0") + ); + } + + @ParameterizedTest + @MethodSource("imdsCredentialProvidersWithSessionToken") + void userAgentString_containsImdsBusinessMetric_WhenUsingInstanceProfileCredentialsWithSessionToken( + IdentityProvider provider, String expected) throws Exception { + + stubImdsResponsesWithSessionToken(); + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream imdsCredentialProvidersWithSessionToken() { + return Stream.of( + Arguments.of(InstanceProfileCredentialsProvider.create(), "m/D,0") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProcessCredentialsProviderUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProcessCredentialsProviderUserAgentTest.java new file mode 100644 index 000000000000..851fb9826036 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProcessCredentialsProviderUserAgentTest.java @@ -0,0 +1,157 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.ProcessCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.DateUtils; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProcessCredentialsProvider correctly includes + * business metrics in the User-Agent header. This test focuses specifically on the + * CREDENTIALS_PROCESS ("w") business metric feature ID. + */ +class ProcessCredentialsProviderUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + + @BeforeEach + public void setup() { + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + } + + private static HttpExecuteResponse mockStsResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + @ParameterizedTest + @MethodSource("processCredentialProviders") + void userAgentString_containsProcessBusinessMetric_WhenUsingProcessCredentials( + IdentityProvider provider, String expected) throws Exception { + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream processCredentialProviders() { + String mockCommand = createMockCredentialsCommand(false); + List mockCommandList = createMockCredentialsCommandList(false); + + return Stream.of( + Arguments.of(ProcessCredentialsProvider.builder() + .command(mockCommand) + .build(), "m/D,w"), + + Arguments.of(ProcessCredentialsProvider.builder() + .command(mockCommandList) + .build(), "m/D,w") + ); + } + + @ParameterizedTest + @MethodSource("processCredentialProvidersWithSessionToken") + void userAgentString_containsProcessBusinessMetric_WhenUsingProcessCredentialsWithSessionToken( + IdentityProvider provider, String expected) throws Exception { + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream processCredentialProvidersWithSessionToken() { + String mockCommand = createMockCredentialsCommand(true); + + return Stream.of( + Arguments.of(ProcessCredentialsProvider.builder() + .command(mockCommand) + .build(), "m/D,w") + ); + } + + private static String createMockCredentialsCommand(boolean includeSessionToken) { + String credentialsJson = createCredentialsJson(includeSessionToken); + + return "echo '" + credentialsJson + "'"; + } + + private static List createMockCredentialsCommandList(boolean includeSessionToken) { + String credentialsJson = createCredentialsJson(includeSessionToken); + + // Use echo command as a list + return Arrays.asList("echo", credentialsJson); + } + + private static String createCredentialsJson(boolean includeSessionToken) { + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"Version\": 1,"); + json.append("\"AccessKeyId\": \"test-access-key\","); + json.append("\"SecretAccessKey\": \"test-secret-key\""); + + if (includeSessionToken) { + json.append(",\"SessionToken\": \"test-session-token\""); + } + + // Add expiration time (1 hour from now) + String expiration = DateUtils.formatIso8601Date(Instant.now().plus(1, ChronoUnit.HOURS)); + json.append(",\"Expiration\": \"").append(expiration).append("\""); + + json.append("}"); + return json.toString(); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleFailureUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleFailureUserAgentTest.java new file mode 100644 index 000000000000..50f63b8145f5 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleFailureUserAgentTest.java @@ -0,0 +1,206 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProfileCredentialsProvider correctly handles + * business metrics in the User-Agent header when assume role fails and + * falls back to the next provider in the chain. + * + * This test simulates this example: + * - Profile A: assume role with source_profile = B (fails) + * - Profile B: basic credentials (access key + secret key) + * - Falls back to EnvironmentVariableCredentialsProvider (succeeds) + * + * Expected behavior: When assume role fails, "o" and "n" are removed from + * business metrics, and the chain continues to the next provider. + * Final business metrics should only contain the successful provider's metrics. + */ +class ProfileAssumeRoleFailureUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + private Path tempConfigFile; + + @BeforeEach + public void setup() throws IOException { + tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "source_profile = B\n" + + "\n" + + "[profile B]\n" + + "aws_access_key_id = abc123\n" + + "aws_secret_access_key = def456\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + // Mock STS AssumeRole failure response (403 Forbidden) + mockHttpClient.stubNextResponse(mockStsAssumeRoleFailureResponse()); + // Mock successful GetCallerIdentity response for fallback provider + mockHttpClient.stubNextResponse(mockStsGetCallerIdentityResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tempConfigFile != null && Files.exists(tempConfigFile)) { + Files.delete(tempConfigFile); + } + } + + private static HttpExecuteResponse mockStsAssumeRoleFailureResponse() { + String responseBody = "" + + "" + + "" + + "Sender" + + "AccessDenied" + + "User: arn:aws:iam::123456789:user/testuser is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789:role/RoleA" + + "" + + "c6104cbe-af31-11e0-8154-cbc7ccf896c7" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(403).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsGetCallerIdentityResponse() { + String responseBody = "" + + "" + + "" + + "arn:aws:iam::123456789:user/testuser" + + "AIDACKCEVSQ6C2EXAMPLE" + + "123456789" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("profileAssumeRoleFailureProviders") + void userAgentString_containsOnlySuccessfulProviderBusinessMetrics_WhenAssumeRoleFails( + IdentityProvider provider, String expected) throws Exception { + + System.setProperty("aws.accessKeyId", "AKIAIOSFODNN7EXAMPLE"); + System.setProperty("aws.secretAccessKey", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + String userAgent = userAgentHeaders.get(0); + + assertThat(userAgent).contains(expected); + + // Verify that failed assume role metrics are not present + // The "o" (CREDENTIALS_PROFILE_SOURCE_PROFILE) and "n" (CREDENTIALS_PROFILE) + // should be removed when assume role fails + assertThat(userAgent).doesNotContain("o"); + assertThat(userAgent).doesNotContain("n"); + + } catch (Exception e) { + } finally { + System.clearProperty("aws.accessKeyId"); + System.clearProperty("aws.secretAccessKey"); + } + } + + private static Stream profileAssumeRoleFailureProviders() throws IOException { + // Create temporary config file with profile assume role scenario + Path tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "source_profile = B\n" + + "\n" + + "[profile B]\n" + + "aws_access_key_id = abc123\n" + + "aws_secret_access_key = def456\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + // Create ProfileFile from temporary config + ProfileFile profileFile = ProfileFile.builder() + .content(tempConfigFile) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + // Create a credentials provider chain that includes: + // 1. ProfileCredentialsProvider (will fail assume role) + // 2. EnvironmentVariableCredentialsProvider (will succeed) + ProfileCredentialsProvider profileProvider = ProfileCredentialsProvider.builder() + .profileFile(profileFile) + .profileName("A") + .build(); + + EnvironmentVariableCredentialsProvider envProvider = EnvironmentVariableCredentialsProvider.create(); + + AwsCredentialsProviderChain chainProvider = AwsCredentialsProviderChain.of( + profileProvider, + envProvider + ); + + return Stream.of( + // Expected: Only environment variable provider business metrics (g = CREDENTIALS_ENV_VARS) + // The failed profile assume role metrics (o, n) should be removed + Arguments.of(chainProvider, "m/D,g") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleUserAgentTest.java new file mode 100644 index 000000000000..678fb4dc5d0d --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileAssumeRoleUserAgentTest.java @@ -0,0 +1,176 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProfileCredentialsProvider correctly includes + * business metrics in the User-Agent header for profile assume role scenarios. + * + * This test simulates this example: + * - Profile A: assume role with source_profile = B + * - Profile B: basic credentials (access key + secret key) + * + * Expected business metrics: "o" (CREDENTIALS_PROFILE_SOURCE_PROFILE), + * "n" (CREDENTIALS_PROFILE), "i" (CREDENTIALS_STS_ASSUME_ROLE) + */ +class ProfileAssumeRoleUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + private Path tempConfigFile; + + @BeforeEach + public void setup() throws IOException { + tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "source_profile = B\n" + + "\n" + + "[profile B]\n" + + "aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n" + + "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsAssumeRoleResponse()); + mockHttpClient.stubNextResponse(mockStsGetCallerIdentityResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tempConfigFile != null && Files.exists(tempConfigFile)) { + Files.delete(tempConfigFile); + } + } + + private static HttpExecuteResponse mockStsAssumeRoleResponse() { + String responseBody = "" + + "" + + "" + + "" + + "ASIAIOSFODNN7EXAMPLE" + + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + + "session-token" + + "2024-12-31T23:59:59Z" + + "" + + "" + + "AROA3XFRBF535PLBQX4MJ:aws-sdk-java-1234567890" + + "arn:aws:sts::123456789:assumed-role/RoleA/aws-sdk-java-1234567890" + + "" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsGetCallerIdentityResponse() { + String responseBody = "" + + "" + + "" + + "arn:aws:sts::123456789:assumed-role/RoleA/aws-sdk-java-1234567890" + + "AROA3XFRBF535PLBQX4MJ:aws-sdk-java-1234567890" + + "123456789" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("profileAssumeRoleCredentialProviders") + void userAgentString_containsProfileAssumeRoleBusinessMetrics_WhenUsingProfileAssumeRole( + IdentityProvider provider, String expected) throws Exception { + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } catch (Exception e) { + } + } + + private static Stream profileAssumeRoleCredentialProviders() throws IOException { + // Create temporary config file with profile assume role scenario + Path tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "source_profile = B\n" + + "\n" + + "[profile B]\n" + + "aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n" + + "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + // Create ProfileFile from temporary config + ProfileFile profileFile = ProfileFile.builder() + .content(tempConfigFile) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + return Stream.of( + Arguments.of(ProfileCredentialsProvider.builder() + .profileFile(profileFile) + .profileName("A") + .build(), "m/D,o,n,i") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceImdsFailureUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceImdsFailureUserAgentTest.java new file mode 100644 index 000000000000..e65e53ac1696 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceImdsFailureUserAgentTest.java @@ -0,0 +1,212 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProfileCredentialsProvider correctly handles + * business metrics in the User-Agent header when credential_source IMDS + * provider fails and falls back to the next provider in the chain. + * + * This test simulates this example: + * - Profile A: assume role with credential_source = Ec2InstanceMetadata + * - IMDS credentials provider fails (emits "p" but not "0") + * - Falls back to EnvironmentVariableCredentialsProvider (succeeds) + * + * Expected behavior: When IMDS fails, "p" is removed from business metrics, + * and the chain continues to the next provider. + * Final business metrics should only contain the successful provider's metrics. + */ +class ProfileCredentialSourceImdsFailureUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + private Path tempConfigFile; + + @BeforeEach + public void setup() throws IOException { + // Create temporary config file with profile credential source scenario + tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + // Mock IMDS token request success + mockHttpClient.stubNextResponse(mockImdsTokenResponse()); + // Mock IMDS credentials request failure (404 Not Found) + mockHttpClient.stubNextResponse(mockImdsCredentialsFailureResponse()); + // Mock successful GetCallerIdentity response for fallback provider + mockHttpClient.stubNextResponse(mockStsGetCallerIdentityResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tempConfigFile != null && Files.exists(tempConfigFile)) { + Files.delete(tempConfigFile); + } + } + + private static HttpExecuteResponse mockImdsTokenResponse() { + String responseBody = "AQAAANpEq2k-c8BtmxvWBHyQLjKJEc6DEBhQ3oP5wVxVSKWHhH_SqA=="; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockImdsCredentialsFailureResponse() { + String responseBody = "\n" + + "\n" + + "\n" + + " \n" + + " 404 - Not Found\n" + + " \n" + + " \n" + + "

404 - Not Found

\n" + + " \n" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(404).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsGetCallerIdentityResponse() { + String responseBody = "" + + "" + + "" + + "arn:aws:iam::123456789:user/testuser" + + "AIDACKCEVSQ6C2EXAMPLE" + + "123456789" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("profileCredentialSourceImdsFailureProviders") + void userAgentString_containsOnlySuccessfulProviderBusinessMetrics_WhenCredentialSourceImdsProviderFails( + IdentityProvider provider, String expected) throws Exception { + + // Set environment variables for EnvironmentVariableCredentialsProvider + System.setProperty("aws.accessKeyId", "AKIAIOSFODNN7EXAMPLE"); + System.setProperty("aws.secretAccessKey", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + String userAgent = userAgentHeaders.get(0); + + assertThat(userAgent).contains(expected); + + // Verify that failed credential source metrics are not present + // The "p" (CREDENTIALS_PROFILE_CREDENTIAL_SOURCE) should be removed when IMDS fails + // The "0" (CREDENTIALS_IMDS) should not be present since IMDS never succeeded + assertThat(userAgent).doesNotContain("p"); + assertThat(userAgent).doesNotContain("0"); + + } catch (Exception e) { + + } finally { + System.clearProperty("aws.accessKeyId"); + System.clearProperty("aws.secretAccessKey"); + } + } + + private static Stream profileCredentialSourceImdsFailureProviders() throws IOException { + Path tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + ProfileFile profileFile = ProfileFile.builder() + .content(tempConfigFile) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + // Create a credentials provider chain that includes: + // 1. ProfileCredentialsProvider (IMDS fails) + // 2. EnvironmentVariableCredentialsProvider (succeed) + ProfileCredentialsProvider profileProvider = ProfileCredentialsProvider.builder() + .profileFile(profileFile) + .profileName("A") + .build(); + + EnvironmentVariableCredentialsProvider envProvider = EnvironmentVariableCredentialsProvider.create(); + + AwsCredentialsProviderChain chainProvider = AwsCredentialsProviderChain.of( + profileProvider, + envProvider + ); + + return Stream.of( + // Expected: Only environment variable provider business metrics (g = CREDENTIALS_ENV_VARS) + // The failed credential source metric (p) should be removed + Arguments.of(chainProvider, "m/D,g") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceStsFailureUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceStsFailureUserAgentTest.java new file mode 100644 index 000000000000..b71dbcfd7a51 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceStsFailureUserAgentTest.java @@ -0,0 +1,229 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProfileCredentialsProvider correctly handles + * business metrics in the User-Agent header when credential_source succeeds + * but assume role fails and falls back to the next provider in the chain. + * + * This test simulates this example: + * - Profile A: assume role with credential_source = Ec2InstanceMetadata + * - IMDS credentials provider succeeds (emits "p", "0") + * - Assume role service call fails + * - Falls back to EnvironmentVariableCredentialsProvider (succeeds) + * + * Expected behavior: When assume role fails, "p" and "0" are removed from + * business metrics, and the chain continues to the next provider. + * Final business metrics should only contain the successful provider's metrics. + */ +class ProfileCredentialSourceStsFailureUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + private Path tempConfigFile; + + @BeforeEach + public void setup() throws IOException { + // Create temporary config file with profile credential source scenario + tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + // Mock successful IMDS responses + mockHttpClient.stubNextResponse(mockImdsTokenResponse()); + mockHttpClient.stubNextResponse(mockImdsCredentialsResponse()); + // Mock STS AssumeRole failure response (403 Forbidden) + mockHttpClient.stubNextResponse(mockStsAssumeRoleFailureResponse()); + // Mock successful GetCallerIdentity response for fallback provider + mockHttpClient.stubNextResponse(mockStsGetCallerIdentityResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tempConfigFile != null && Files.exists(tempConfigFile)) { + Files.delete(tempConfigFile); + } + } + + private static HttpExecuteResponse mockImdsTokenResponse() { + String responseBody = "AQAAANpEq2k-c8BtmxvWBHyQLjKJEc6DEBhQ3oP5wVxVSKWHhH_SqA=="; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockImdsCredentialsResponse() { + String responseBody = "{\n" + + " \"Code\" : \"Success\",\n" + + " \"LastUpdated\" : \"2024-01-01T00:00:00Z\",\n" + + " \"Type\" : \"AWS-HMAC\",\n" + + " \"AccessKeyId\" : \"AKIAIOSFODNN7EXAMPLE\",\n" + + " \"SecretAccessKey\" : \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\",\n" + + " \"Token\" : \"token\",\n" + + " \"Expiration\" : \"2024-12-31T23:59:59Z\"\n" + + "}"; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsAssumeRoleFailureResponse() { + String responseBody = "" + + "" + + "" + + "Sender" + + "AccessDenied" + + "User: arn:aws:iam::123456789:user/testuser is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789:role/RoleA" + + "" + + "c6104cbe-af31-11e0-8154-cbc7ccf896c7" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(403).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsGetCallerIdentityResponse() { + String responseBody = "" + + "" + + "" + + "arn:aws:iam::123456789:user/testuser" + + "AIDACKCEVSQ6C2EXAMPLE" + + "123456789" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("profileCredentialSourceStsFailureProviders") + void userAgentString_containsOnlySuccessfulProviderBusinessMetrics_WhenCredentialSourceSucceedsButAssumeRoleFails( + IdentityProvider provider, String expected) throws Exception { + + System.setProperty("aws.accessKeyId", "AKIAIOSFODNN7EXAMPLE"); + System.setProperty("aws.secretAccessKey", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + String userAgent = userAgentHeaders.get(0); + + assertThat(userAgent).contains(expected); + + // Verify that failed credential source metrics are NOT present + // The "p" (CREDENTIALS_PROFILE_CREDENTIAL_SOURCE) and "0" (CREDENTIALS_IMDS) + // should be removed when assume role fails + assertThat(userAgent).doesNotContain("p"); + assertThat(userAgent).doesNotContain("0"); + + } catch (Exception e) { + } finally { + System.clearProperty("aws.accessKeyId"); + System.clearProperty("aws.secretAccessKey"); + } + } + + private static Stream profileCredentialSourceStsFailureProviders() throws IOException { + // Create temporary config file with profile credential source scenario + Path tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + // Create ProfileFile from temporary config + ProfileFile profileFile = ProfileFile.builder() + .content(tempConfigFile) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + // Create a credentials provider chain that includes: + // 1. ProfileCredentialsProvider (IMDS succeeds, assume role fails) + // 2. EnvironmentVariableCredentialsProvider (will succeed) + ProfileCredentialsProvider profileProvider = ProfileCredentialsProvider.builder() + .profileFile(profileFile) + .profileName("A") + .build(); + + EnvironmentVariableCredentialsProvider envProvider = EnvironmentVariableCredentialsProvider.create(); + + AwsCredentialsProviderChain chainProvider = AwsCredentialsProviderChain.of( + profileProvider, + envProvider + ); + + return Stream.of( + // Expected: Only environment variable provider business metrics (g = CREDENTIALS_ENV_VARS) + // The failed credential source metrics (p, 0) should be removed + Arguments.of(chainProvider, "m/D,g") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceUserAgentTest.java new file mode 100644 index 000000000000..cb9f4d0d02b7 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/ProfileCredentialSourceUserAgentTest.java @@ -0,0 +1,194 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that ProfileCredentialsProvider correctly includes + * business metrics in the User-Agent header for profile assume role scenarios + * with credential_source. + * + * This file tests this ex: + * - Profile A: assume role with credential_source = Ec2InstanceMetadata + * + * Expected business metrics: "p" (CREDENTIALS_PROFILE_CREDENTIAL_SOURCE), + * "0" (CREDENTIALS_IMDS), "i" (CREDENTIALS_STS_ASSUME_ROLE) + */ +class ProfileCredentialSourceUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + private Path tempConfigFile; + + @BeforeEach + public void setup() throws IOException { + tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockImdsTokenResponse()); + mockHttpClient.stubNextResponse(mockImdsCredentialsResponse()); + mockHttpClient.stubNextResponse(mockStsAssumeRoleResponse()); + mockHttpClient.stubNextResponse(mockStsGetCallerIdentityResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tempConfigFile != null && Files.exists(tempConfigFile)) { + Files.delete(tempConfigFile); + } + } + + private static HttpExecuteResponse mockImdsTokenResponse() { + String responseBody = "AQAAANpEq2k-c8BtmxvWBHyQLjKJEc6DEBhQ3oP5wVxVSKWHhH_SqA=="; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockImdsCredentialsResponse() { + String responseBody = "{\n" + + " \"Code\" : \"Success\",\n" + + " \"LastUpdated\" : \"2024-01-01T00:00:00Z\",\n" + + " \"Type\" : \"AWS-HMAC\",\n" + + " \"AccessKeyId\" : \"AKIAIOSFODNN7EXAMPLE\",\n" + + " \"SecretAccessKey\" : \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\",\n" + + " \"Token\" : \"token\",\n" + + " \"Expiration\" : \"2024-12-31T23:59:59Z\"\n" + + "}"; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsAssumeRoleResponse() { + String responseBody = "" + + "" + + "" + + "" + + "ASIAIOSFODNN7EXAMPLE" + + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + + "session-token" + + "2024-12-31T23:59:59Z" + + "" + + "" + + "AROA3XFRBF535PLBQX4MJ:aws-sdk-java-1234567890" + + "arn:aws:sts::123456789:assumed-role/RoleA/aws-sdk-java-1234567890" + + "" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + private static HttpExecuteResponse mockStsGetCallerIdentityResponse() { + String responseBody = "" + + "" + + "" + + "arn:aws:sts::123456789:assumed-role/RoleA/aws-sdk-java-1234567890" + + "AROA3XFRBF535PLBQX4MJ:aws-sdk-java-1234567890" + + "123456789" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("profileCredentialSourceProviders") + void userAgentString_containsProfileCredentialSourceBusinessMetrics_WhenUsingProfileCredentialSource( + IdentityProvider provider, String expected) throws Exception { + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } catch (Exception e) { + } + } + + private static Stream profileCredentialSourceProviders() throws IOException { + Path tempConfigFile = Files.createTempFile("aws-config-", ".tmp"); + String configContent = + "[profile A]\n" + + "role_arn = arn:aws:iam::123456789:role/RoleA\n" + + "credential_source = Ec2InstanceMetadata\n"; + + Files.write(tempConfigFile, configContent.getBytes()); + + ProfileFile profileFile = ProfileFile.builder() + .content(tempConfigFile) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + return Stream.of( + Arguments.of(ProfileCredentialsProvider.builder() + .profileFile(profileFile) + .profileName("A") + .build(), "m/D,p,0,i") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/SystemPropertyCredentialsProviderUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/SystemPropertyCredentialsProviderUserAgentTest.java new file mode 100644 index 000000000000..de57169198fb --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/SystemPropertyCredentialsProviderUserAgentTest.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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that SystemPropertyCredentialsProvider correctly includes + * business metrics in the User-Agent header. This test focuses specifically on the + * CREDENTIALS_JVM_SYSTEM_PROPERTIES ("f") business metric feature ID. + */ +class SystemPropertyCredentialsProviderUserAgentTest { + + private MockSyncHttpClient mockHttpClient; + + @BeforeEach + public void setup() { + + System.setProperty("aws.accessKeyId", "test-access-key"); + System.setProperty("aws.secretAccessKey", "test-secret-key"); + + // Setup mock HTTP client for STS calls + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + } + + @AfterAll + public static void teardown() { + System.clearProperty("aws.accessKeyId"); + System.clearProperty("aws.secretAccessKey"); + System.clearProperty("aws.sessionToken"); + } + + private static HttpExecuteResponse mockStsResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + } + + @ParameterizedTest + @MethodSource("systemPropertyCredentialProviders") + void userAgentString_containsSystemPropertyBusinessMetric_WhenUsingSystemPropertyCredentials( + IdentityProvider provider, String expected) throws Exception { + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream systemPropertyCredentialProviders() { + return Stream.of( + Arguments.of(SystemPropertyCredentialsProvider.create(), "m/D,f") + ); + } + + @ParameterizedTest + @MethodSource("systemPropertyCredentialProvidersWithSessionToken") + void userAgentString_containsSystemPropertyBusinessMetric_WhenUsingSystemPropertyCredentialsWithSessionToken( + IdentityProvider provider, String expected) throws Exception { + + System.setProperty("aws.sessionToken", "test-session-token"); + + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } + + private static Stream systemPropertyCredentialProvidersWithSessionToken() { + return Stream.of( + Arguments.of(SystemPropertyCredentialsProvider.create(), "m/D,f") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +} diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java index ffe82176afff..4ee93a9afd32 100644 --- a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java @@ -46,7 +46,7 @@ class UserAgentProviderTest { private MockSyncHttpClient mockHttpClient; @BeforeEach - public void setup() throws UnsupportedEncodingException { + public void setup() { mockHttpClient = new MockSyncHttpClient(); mockHttpClient.stubNextResponse(mockResponse()); } @@ -74,8 +74,8 @@ void userAgentString_containsCredentialProviderNames_IfPresent(IdentityProvider< private static Stream credentialProviders() { return Stream.of( - Arguments.of(StaticCredentialsProvider.create(SESSION_IDENTITY), "stat"), - Arguments.of(StaticCredentialsProvider.create(BASIC_IDENTITY), "stat") + Arguments.of(StaticCredentialsProvider.create(SESSION_IDENTITY), "m/D,e"), + Arguments.of(StaticCredentialsProvider.create(BASIC_IDENTITY), "m/D,e") ); } diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/WebIdentityTokenFileCredentialsProviderUserAgentTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/WebIdentityTokenFileCredentialsProviderUserAgentTest.java new file mode 100644 index 000000000000..b3d47b961a87 --- /dev/null +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/WebIdentityTokenFileCredentialsProviderUserAgentTest.java @@ -0,0 +1,131 @@ +/* + * 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.auth.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that WebIdentityTokenFileCredentialsProvider correctly includes + * business metrics in the User-Agent header. This test focuses specifically on the + * CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN ("h") business metric feature ID. + */ +class WebIdentityTokenFileCredentialsProviderUserAgentTest { + + private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/TestRole"; + private static final String ROLE_SESSION_NAME = "test-session"; + + private MockSyncHttpClient mockHttpClient; + private Path tokenFile; + + @BeforeEach + public void setup() throws IOException { + String existingTokenPath = Paths.get("../../services/sts/src/test/resources/token.jwt").toAbsolutePath().toString(); + byte[] tokenBytes = Files.readAllBytes(Paths.get(existingTokenPath)); + String tokenContent = new String(tokenBytes); + + tokenFile = Files.createTempFile("web-identity-token", ".jwt"); + Files.write(tokenFile, tokenContent.getBytes()); + + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockStsResponse()); + } + + @AfterEach + public void teardown() throws IOException { + if (tokenFile != null && Files.exists(tokenFile)) { + Files.delete(tokenFile); + } + } + + private static HttpExecuteResponse mockStsResponse() { + String responseBody = "" + + "" + + "" + + "" + + "AKIAIOSFODNN7EXAMPLE" + + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + + "session-token" + + "2024-12-31T23:59:59Z" + + "" + + "" + + ""; + + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(responseBody))) + .build(); + } + + @ParameterizedTest + @MethodSource("webIdentityTokenCredentialProvidersWithBuilder") + void userAgentString_containsWebIdentityTokenBusinessMetric_WhenUsingWebIdentityTokenCredentialsWithBuilder( + IdentityProvider provider, String expected) throws Exception { + + try { + stsClient(provider, mockHttpClient).getCallerIdentity(); + + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains(expected); + } catch (Exception e) { + } + } + + private static Stream webIdentityTokenCredentialProvidersWithBuilder() throws IOException { + String existingTokenPath = Paths.get("../../services/sts/src/test/resources/token.jwt").toAbsolutePath().toString(); + + return Stream.of( + Arguments.of(WebIdentityTokenFileCredentialsProvider.builder() + .roleArn(ROLE_ARN) + .roleSessionName(ROLE_SESSION_NAME) + .webIdentityTokenFile(Paths.get(existingTokenPath)) + .build(), "m/D,h") + ); + } + + private static StsClient stsClient(IdentityProvider provider, SdkHttpClient httpClient) { + return StsClient.builder() + .credentialsProvider(provider) + .httpClient(httpClient) + .build(); + } +}