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/HttpCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/HttpCredentialsProvider.java index 31efde720d34..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; /** @@ -49,7 +50,8 @@ interface Builder { - 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/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(); + } +}