diff --git a/.changes/0b5b53ab-70c0-4c1b-a445-8663ae86d6d1.json b/.changes/0b5b53ab-70c0-4c1b-a445-8663ae86d6d1.json new file mode 100644 index 00000000000..d43471dc7c4 --- /dev/null +++ b/.changes/0b5b53ab-70c0-4c1b-a445-8663ae86d6d1.json @@ -0,0 +1,6 @@ +{ + "id": "0b5b53ab-70c0-4c1b-a445-8663ae86d6d1", + "type": "misc", + "description": "The order of credentials resolution in config files has been updated to: static credentials, assume role with source profile OR assume role with named provider, web identity token, SSO session, legacy SSO, process", + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/99a099e1-26c1-4ba1-b0d3-435609ea4e94.json b/.changes/99a099e1-26c1-4ba1-b0d3-435609ea4e94.json new file mode 100644 index 00000000000..abcd64fb670 --- /dev/null +++ b/.changes/99a099e1-26c1-4ba1-b0d3-435609ea4e94.json @@ -0,0 +1,6 @@ +{ + "id": "99a099e1-26c1-4ba1-b0d3-435609ea4e94", + "type": "misc", + "description": "The order of credentials resolution in the credentials provider chain has been updated to: system properties, environment variables, web identity tokens, profile, ECS, EC2", + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/aws-runtime/aws-config/api/aws-config.api b/aws-runtime/aws-config/api/aws-config.api index 05589280306..cf329e7c96e 100644 --- a/aws-runtime/aws-config/api/aws-config.api +++ b/aws-runtime/aws-config/api/aws-config.api @@ -51,6 +51,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentia public final fun getProfileName ()Ljava/lang/String; public final fun getRegion ()Ljava/lang/String; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CloseableCredentialsProvider { @@ -62,6 +63,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvide public fun close ()V public final fun getPlatformProvider ()Laws/smithy/kotlin/runtime/util/PlatformProvider; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider { @@ -70,6 +72,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredential public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getGetEnv ()Lkotlin/jvm/functions/Function1; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CloseableCredentialsProvider { @@ -81,6 +84,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvid public final fun getPlatformProvider ()Laws/smithy/kotlin/runtime/util/PlatformEnvironProvider; public final fun getProfileOverride ()Ljava/lang/String; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/InvalidJsonCredentialsException : aws/sdk/kotlin/runtime/ClientException { @@ -97,6 +101,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsPro public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/util/PlatformProvider;JJ)V public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/util/PlatformProvider;JJILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CloseableCredentialsProvider { @@ -112,6 +117,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsPro public final fun getProfileName ()Ljava/lang/String; public final fun getRegion ()Ljava/lang/String; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/ProviderConfigurationException : aws/sdk/kotlin/runtime/ConfigurationException { @@ -130,6 +136,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProvide public final fun getSsoSessionName ()Ljava/lang/String; public final fun getStartUrl ()Ljava/lang/String; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/SsoTokenProvider : aws/smithy/kotlin/runtime/http/auth/BearerTokenProvider { @@ -150,6 +157,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProv public fun (Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials;)V public final fun getCredentials ()Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider$Builder { @@ -179,6 +187,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredenti public final fun getHttpClient ()Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; public final fun getRegion ()Ljava/lang/String; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider { @@ -192,6 +201,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredent public final fun getRegion ()Ljava/lang/String; public final fun getWebIdentityParameters ()Laws/sdk/kotlin/runtime/auth/credentials/AssumeRoleWithWebIdentityParameters; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider$Companion { @@ -199,12 +209,24 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredent public static synthetic fun fromEnvironment-TUY-ock$default (Laws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLaws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;ILjava/lang/Object;)Laws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider; } +public final class aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CloseableCredentialsProvider { + public fun ()V + public fun (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;Ljava/lang/String;)V + public synthetic fun (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public final fun getHttpClient ()Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; + public final fun getPlatformProvider ()Laws/smithy/kotlin/runtime/util/PlatformProvider; + public final fun getRegion ()Ljava/lang/String; + public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProvider : aws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider { public fun ()V public fun (Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getGetProperty ()Lkotlin/jvm/functions/Function1; public fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/auth/credentials/internal/ManagedBearerTokenProviderKt { diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProvider.kt index c690d48b796..5b849ec0a35 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProvider.kt @@ -7,10 +7,9 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting import aws.sdk.kotlin.runtime.config.imds.ImdsClient -import aws.smithy.kotlin.runtime.auth.awscredentials.CachedCredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderChain +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.http.engine.DefaultHttpEngine import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine @@ -23,11 +22,12 @@ import aws.smithy.kotlin.runtime.util.PlatformProvider * * Resolution order: * - * 1. Environment variables ([EnvironmentCredentialsProvider]) - * 2. Profile ([ProfileCredentialsProvider]) + * 1. System properties ([SystemPropertyCredentialsProvider]) + * 2. Environment variables ([EnvironmentCredentialsProvider]) * 3. Web Identity Tokens ([StsWebIdentityCredentialsProvider]] - * 4. ECS (IAM roles for tasks) ([EcsCredentialsProvider]) - * 5. EC2 Instance Metadata (IMDSv2) ([ImdsCredentialsProvider]) + * 4. Profile ([ProfileCredentialsProvider]) + * 5. ECS (IAM roles for tasks) ([EcsCredentialsProvider]) + * 6. EC2 Instance Metadata (IMDSv2) ([ImdsCredentialsProvider]) * * The chain is decorated with a [CachedCredentialsProvider]. * @@ -54,9 +54,9 @@ public class DefaultChainCredentialsProvider constructor( private val chain = CredentialsProviderChain( SystemPropertyCredentialsProvider(platformProvider::getProperty), EnvironmentCredentialsProvider(platformProvider::getenv), - ProfileCredentialsProvider(profileName = profileName, platformProvider = platformProvider, httpClient = engine, region = region), // STS web identity provider can be constructed from either the profile OR 100% from the environment StsWebIdentityProvider(platformProvider = platformProvider, httpClient = engine, region = region), + ProfileCredentialsProvider(profileName = profileName, platformProvider = platformProvider, httpClient = engine, region = region), EcsCredentialsProvider(platformProvider, engine), ImdsCredentialsProvider( client = lazy { @@ -79,20 +79,22 @@ public class DefaultChainCredentialsProvider constructor( engine.closeIfCloseable() } } + + override fun toString(): String = this.simpleClassName + ": " + this.chain } /** * Wrapper around [StsWebIdentityCredentialsProvider] that delays any exceptions until [resolve] is invoked. * This allows it to be part of the default chain and any failures result in the chain to move onto the next provider. */ -private class StsWebIdentityProvider( - val platformProvider: PlatformProvider = PlatformProvider.System, - val httpClient: HttpClientEngine? = null, - val region: String? = null, +public class StsWebIdentityProvider( + public val platformProvider: PlatformProvider = PlatformProvider.System, + public val httpClient: HttpClientEngine? = null, + public val region: String? = null, ) : CloseableCredentialsProvider { override suspend fun resolve(attributes: Attributes): Credentials { val wrapped = StsWebIdentityCredentialsProvider.fromEnvironment(platformProvider = platformProvider, httpClient = httpClient, region = region) - return wrapped.resolve(attributes) + return wrapped.resolve(attributes).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN) } override fun close() { } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvider.kt index e0fe1ce688c..06afaf043c5 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProvider.kt @@ -7,11 +7,10 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.ErrorMetadata -import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.client.endpoints.Endpoint import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.config.resolve @@ -79,6 +78,7 @@ public class EcsCredentialsProvider( override suspend fun resolve(attributes: Attributes): Credentials { val logger = coroutineContext.logger() + val authToken = loadAuthToken() val relativeUri = AwsSdkSetting.AwsContainerCredentialsRelativeUri.resolve(platformProvider) val fullUri = AwsSdkSetting.AwsContainerCredentialsFullUri.resolve(platformProvider) @@ -113,7 +113,7 @@ public class EcsCredentialsProvider( logger.debug { "obtained credentials from container metadata service; expiration=${creds.expiration?.format(TimestampFormat.ISO_8601)}" } - return creds + return creds.withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_HTTP) } private suspend fun loadAuthToken(): String? { @@ -197,6 +197,8 @@ public class EcsCredentialsProvider( httpClient.closeIfCloseable() } } + + override fun toString(): String = this.simpleClassName } private class EcsCredentialsDeserializer : HttpDeserialize { diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProvider.kt index c914e319e70..59a0114e235 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProvider.kt @@ -7,8 +7,11 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awscredentials.simpleClassName import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.telemetry.logging.trace import aws.smithy.kotlin.runtime.util.PlatformProvider @@ -35,12 +38,15 @@ public class EnvironmentCredentialsProvider( coroutineContext.trace { "Attempting to load credentials from env vars $ACCESS_KEY_ID/$SECRET_ACCESS_KEY/$SESSION_TOKEN" } + return credentials( accessKeyId = requireEnv(ACCESS_KEY_ID), secretAccessKey = requireEnv(SECRET_ACCESS_KEY), sessionToken = getEnv(SESSION_TOKEN), providerName = PROVIDER_NAME, accountId = getEnv(ACCOUNT_ID), - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS) } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt index ff6d53a0c41..495e3d810d6 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt @@ -9,11 +9,9 @@ import aws.sdk.kotlin.runtime.config.AwsSdkSetting import aws.sdk.kotlin.runtime.config.imds.EC2MetadataError import aws.sdk.kotlin.runtime.config.imds.ImdsClient import aws.sdk.kotlin.runtime.config.imds.InstanceMetadataProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException -import aws.smithy.kotlin.runtime.auth.awscredentials.DEFAULT_CREDENTIALS_REFRESH_SECONDS +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.config.resolve import aws.smithy.kotlin.runtime.http.HttpStatusCode @@ -110,7 +108,7 @@ public class ImdsCredentialsProvider( resp.sessionToken, resp.expiration, PROVIDER_NAME, - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) creds.also { mu.withLock { previousCredentials = it } @@ -151,4 +149,6 @@ public class ImdsCredentialsProvider( } else -> null } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProvider.kt index 631dc3c237e..5a1c3a3761b 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProvider.kt @@ -5,9 +5,9 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer import aws.smithy.kotlin.runtime.telemetry.logging.logger @@ -67,15 +67,19 @@ public class ProcessCredentialsProvider( val deserializer = JsonDeserializer(payload) return when (val resp = deserializeJsonProcessCredentials(deserializer)) { - is JsonCredentialsResponse.SessionCredentials -> credentials( - resp.accessKeyId, - resp.secretAccessKey, - resp.sessionToken, - resp.expiration ?: Instant.MAX_VALUE, - PROVIDER_NAME, - resp.accountId, - ) + is JsonCredentialsResponse.SessionCredentials -> { + credentials( + resp.accessKeyId, + resp.secretAccessKey, + resp.sessionToken, + resp.expiration ?: Instant.MAX_VALUE, + PROVIDER_NAME, + resp.accountId, + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROCESS) + } else -> throw CredentialsProviderException("Credentials response was not of expected format") } } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider.kt index a2b3ad6cea5..81935c0be9e 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider.kt @@ -9,15 +9,18 @@ import aws.sdk.kotlin.runtime.InternalSdkApi import aws.sdk.kotlin.runtime.auth.credentials.profile.LeafProvider import aws.sdk.kotlin.runtime.auth.credentials.profile.ProfileChain import aws.sdk.kotlin.runtime.auth.credentials.profile.RoleArn +import aws.sdk.kotlin.runtime.auth.credentials.profile.RoleArnSource import aws.sdk.kotlin.runtime.client.AwsClientOption import aws.sdk.kotlin.runtime.config.AwsSdkSetting import aws.sdk.kotlin.runtime.config.imds.ImdsClient import aws.sdk.kotlin.runtime.config.profile.AwsConfigurationSource import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetrics import aws.sdk.kotlin.runtime.region.resolveRegion -import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awscredentials.* +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine import aws.smithy.kotlin.runtime.io.closeIfCloseable @@ -85,6 +88,7 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( public val httpClient: HttpClientEngine? = null, public val configurationSource: AwsConfigurationSource? = null, ) : CloseableCredentialsProvider { + private val credentialsBusinessMetrics: MutableSet = mutableSetOf() public constructor( profileName: String? = null, @@ -130,12 +134,21 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( chain.roles.forEach { roleArn -> logger.debug { "Assuming role `${roleArn.roleArn}`" } + if (roleArn.source == RoleArnSource.SOURCE_PROFILE) { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_SOURCE_PROFILE) + } + val assumeProvider = roleArn.toCredentialsProvider(creds, region) + + creds.attributes.getOrNull(BusinessMetrics)?.forEach { metric -> + credentialsBusinessMetrics.add(metric) + } + creds = assumeProvider.resolve(attributes) } logger.debug { "Obtained credentials from profile; expiration=${creds.expiration?.format(TimestampFormat.ISO_8601)}" } - return creds + return creds.withBusinessMetrics(credentialsBusinessMetrics) } override fun close() { @@ -146,10 +159,13 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( private suspend fun LeafProvider.toCredentialsProvider(region: LazyAsyncValue): CredentialsProvider = when (this) { - is LeafProvider.NamedSource -> namedProviders[name] - ?: throw ProviderConfigurationException("unknown credentials source: $name") + is LeafProvider.NamedSource -> namedProviders[name].also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_NAMED_PROVIDER) + } ?: throw ProviderConfigurationException("unknown credentials source: $name") - is LeafProvider.AccessKey -> StaticCredentialsProvider(credentials) + is LeafProvider.AccessKey -> StaticCredentialsProvider(credentials).also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE) + } is LeafProvider.WebIdentityTokenRole -> StsWebIdentityCredentialsProvider( roleArn, @@ -158,7 +174,9 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( roleSessionName = sessionName, platformProvider = platformProvider, httpClient = httpClient, - ) + ).also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN) + } is LeafProvider.SsoSession -> SsoCredentialsProvider( accountId = ssoAccountId, @@ -168,7 +186,9 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( ssoSessionName = ssoSessionName, httpClient = httpClient, platformProvider = platformProvider, - ) + ).also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_SSO) + } is LeafProvider.LegacySso -> SsoCredentialsProvider( accountId = ssoAccountId, @@ -177,9 +197,13 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( ssoRegion = ssoRegion, httpClient = httpClient, platformProvider = platformProvider, - ) + ).also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_SSO_LEGACY) + } - is LeafProvider.Process -> ProcessCredentialsProvider(command) + is LeafProvider.Process -> ProcessCredentialsProvider(command).also { + credentialsBusinessMetrics.add(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_PROCESS) + } } private suspend fun RoleArn.toCredentialsProvider( @@ -202,4 +226,6 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor( is LeafProvider.LegacySso -> "single sign-on (legacy)" is LeafProvider.Process -> "process" } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProvider.kt index 42f8dc9c036..3cb4304b5f8 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProvider.kt @@ -8,13 +8,12 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.sso.SsoClient import aws.sdk.kotlin.runtime.auth.credentials.internal.sso.getRoleCredentials -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.client.SdkClientOption import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine -import aws.smithy.kotlin.runtime.serde.json.* import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.telemetry.telemetryProvider import aws.smithy.kotlin.runtime.time.Clock @@ -120,7 +119,7 @@ public class SsoCredentialsProvider public constructor( val roleCredentials = resp.roleCredentials ?: throw CredentialsProviderException("Expected SSO roleCredentials to not be null") - return credentials( + val creds = credentials( accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in SSO roleCredentials response" }, secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in SSO roleCredentials response" }, sessionToken = roleCredentials.sessionToken, @@ -128,6 +127,12 @@ public class SsoCredentialsProvider public constructor( PROVIDER_NAME, accountId = accountId, ) + + return if (ssoTokenProvider != null) { + creds.withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_SSO) + } else { + creds.withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_SSO_LEGACY) + } } // non sso-session legacy token flow @@ -138,4 +143,6 @@ public class SsoCredentialsProvider public constructor( return token } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider.kt index 69d45dfd5ac..bee1fa38e54 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider.kt @@ -8,6 +8,7 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awscredentials.simpleClassName import aws.smithy.kotlin.runtime.collections.Attributes private const val PROVIDER_NAME = "Static" @@ -51,4 +52,6 @@ public class StaticCredentialsProvider(public val credentials: Credentials) : Cr return StaticCredentialsProvider(this) } } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProvider.kt index a6765dc2a72..e84781e95ea 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProvider.kt @@ -13,10 +13,9 @@ import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.PolicyDescript import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.RegionDisabledException import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.Tag import aws.sdk.kotlin.runtime.config.AwsSdkSetting -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException -import aws.smithy.kotlin.runtime.auth.awscredentials.DEFAULT_CREDENTIALS_REFRESH_SECONDS +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.client.SdkClientOption import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.config.resolve @@ -149,8 +148,10 @@ public class StsAssumeRoleCredentialsProvider( expiration = roleCredentials.expiration, providerName = PROVIDER_NAME, accountId = accountId, - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE) } + + override fun toString(): String = this.simpleClassName } /** diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider.kt index 52ffc27285c..3f5147f1968 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProvider.kt @@ -11,10 +11,9 @@ import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.StsClient import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.assumeRoleWithWebIdentity import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.PolicyDescriptorType import aws.sdk.kotlin.runtime.config.AwsSdkSetting -import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider -import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException -import aws.smithy.kotlin.runtime.auth.awscredentials.DEFAULT_CREDENTIALS_REFRESH_SECONDS +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.smithy.kotlin.runtime.auth.awscredentials.* import aws.smithy.kotlin.runtime.client.SdkClientOption import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.config.EnvironmentSetting @@ -107,6 +106,7 @@ public class StsWebIdentityCredentialsProvider( override suspend fun resolve(attributes: Attributes): Credentials { val logger = coroutineContext.logger() logger.debug { "retrieving assumed credentials via web identity" } + val provider = this val params = provider.webIdentityParameters @@ -152,8 +152,10 @@ public class StsWebIdentityCredentialsProvider( expiration = roleCredentials.expiration, providerName = PROVIDER_NAME, accountId = accountId, - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE_WEB_ID) } + + override fun toString(): String = this.simpleClassName } /** diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProvider.kt index 2ad694db34b..db610022604 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProvider.kt @@ -7,8 +7,11 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awscredentials.simpleClassName import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.telemetry.logging.trace import aws.smithy.kotlin.runtime.util.PlatformProvider @@ -35,12 +38,15 @@ public class SystemPropertyCredentialsProvider( coroutineContext.trace { "Attempting to load credentials from system properties $ACCESS_KEY_ID/$SECRET_ACCESS_KEY/$SESSION_TOKEN" } + return credentials( accessKeyId = requireProperty(ACCESS_KEY_ID), secretAccessKey = requireProperty(SECRET_ACCESS_KEY), sessionToken = getProperty(SESSION_TOKEN), providerName = PROVIDER_NAME, accountId = getProperty(ACCOUNT_ID), - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_JVM_SYSTEM_PROPERTIES) } + + override fun toString(): String = this.simpleClassName } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/profile/ProfileChain.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/profile/ProfileChain.kt index 41f92abf6d1..b43f803341e 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/profile/ProfileChain.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/profile/ProfileChain.kt @@ -33,6 +33,16 @@ internal data class ProfileChain( val roles: List, ) { companion object { + /** + * Resolves profile chain with the following precedence: + * + * 1. Static credentials + * 2. Assume role with source profile OR assume role with named provider (mutually exclusive) + * 3. Web ID token file & role arn + * 4. SSO session + * 5. Legacy SSO + * 6. Process + */ internal fun resolve(config: AwsSharedConfig): ProfileChain { val visited = mutableSetOf() val chain = mutableListOf() @@ -53,11 +63,9 @@ internal data class ProfileChain( throw ProviderConfigurationException("profile formed an infinite loop: ${visited.joinToString(separator = " -> ")} -> $sourceProfileName") } - // when chaining assume role profiles, SDKs MUST terminate the chain as soon as they hit a profile with static credentials - if (visited.size > 1) { - leaf = profile.staticCredsOrNull() - if (leaf != null) break@loop - } + // static credentials have the highest precedence + leaf = profile.staticCredsOrNull() + if (leaf != null) break@loop // the existence of `role_arn` is the only signal that multiple profiles will be chained val roleArn = profile.roleArnOrNull() @@ -100,6 +108,12 @@ internal data class RoleArn( * ARN of role to assume */ val roleArn: String, + + /** + * The source used to create the [RoleArn] + */ + val source: RoleArnSource? = null, + /** * Session name to pass to the assume role provider */ @@ -111,6 +125,14 @@ internal data class RoleArn( val externalId: String? = null, ) +/** + * Represents the possible sources for creating a [RoleArn]. + */ +internal enum class RoleArnSource { + SOURCE_PROFILE, + CREDENTIALS_SOURCE, +} + internal const val ROLE_ARN = "role_arn" internal const val EXTERNAL_ID = "external_id" internal const val ROLE_SESSION_NAME = "role_session_name" @@ -132,13 +154,23 @@ internal const val SSO_SESSION = "sso_session" internal const val CREDENTIAL_PROCESS = "credential_process" private fun AwsProfile.roleArnOrNull(): RoleArn? { + val validSource = contains(CREDENTIAL_SOURCE) || contains(SOURCE_PROFILE) + + // chained roles have higher precedence than web id token file // web identity tokens are leaf providers, not chained roles - if (contains(WEB_IDENTITY_TOKEN_FILE)) return null + if (!validSource && contains(WEB_IDENTITY_TOKEN_FILE)) return null val roleArn = getOrNull(ROLE_ARN) ?: return null + val roleArnSource = when { + contains(SOURCE_PROFILE) && !contains(CREDENTIAL_SOURCE) -> RoleArnSource.SOURCE_PROFILE + contains(CREDENTIAL_SOURCE) && !contains(SOURCE_PROFILE) -> RoleArnSource.CREDENTIALS_SOURCE + else -> null + } + return RoleArn( roleArn, + roleArnSource, sessionName = getOrNull(ROLE_SESSION_NAME), externalId = getOrNull(EXTERNAL_ID), ) @@ -237,11 +269,12 @@ private fun AwsProfile.ssoSessionCreds(config: AwsSharedConfig): LeafProviderRes } /** - * Attempt to load [LeafProvider.Process] from the current profile or `null` if the current profile does not contain + * Attempt to load [LeafProvider.Process] from the current profile or exception if the current profile does not contain * a credentials process command to execute */ -private fun AwsProfile.processCreds(): LeafProviderResult? { - if (!contains(CREDENTIAL_PROCESS)) return null +private fun AwsProfile.processCreds(): LeafProviderResult { + // Process is last in precedence - credentials not found means no credentials in profile + if (!contains(CREDENTIAL_PROCESS)) return LeafProviderResult.Err("profile ($name) did not contain credential information") val credentialProcess = getOrNull(CREDENTIAL_PROCESS) ?: return LeafProviderResult.Err("profile ($name) missing `$CREDENTIAL_PROCESS`") @@ -257,7 +290,7 @@ private fun AwsProfile.staticCreds(): LeafProviderResult { val secretKey = getOrNull(AWS_SECRET_ACCESS_KEY) val accountId = getOrNull(AWS_ACCOUNT_ID) return when { - accessKeyId == null && secretKey == null -> LeafProviderResult.Err("profile ($name) did not contain credential information") + accessKeyId == null && secretKey == null -> LeafProviderResult.Err("profile ($name) missing `aws_access_key_id` & `aws_secret_access_key`") accessKeyId == null -> LeafProviderResult.Err("profile ($name) missing `aws_access_key_id`") secretKey == null -> LeafProviderResult.Err("profile ($name) missing `aws_secret_access_key`") else -> { @@ -315,7 +348,6 @@ private fun AwsProfile.leafProvider(config: AwsSharedConfig): LeafProvider { return webIdentityTokenCreds() .orElse { ssoSessionCreds(config) } .orElse(::legacySsoCreds) - .orElse(::processCreds) - .unwrapOrElse(::staticCreds) + .unwrapOrElse(::processCreds) .unwrap() } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_assume_role/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_assume_role/test-case.json index 34730c3ab04..e2c9a8dd33f 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_assume_role/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_assume_role/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "secretkeycorrect", "session_token": "tokencorrect", "expiry": 1632249686, - "accountId": "130633740322" + "accountId": "130633740322", + "business_metrics": ["p","z", "i"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_credentials/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_credentials/test-case.json index a401eea0aa4..de4693e51b6 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_credentials/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/ecs_credentials/test-case.json @@ -6,7 +6,8 @@ "access_key_id": "ASIARCORRECT", "secret_access_key": "secretkeycorrect", "session_token": "tokencorrect", - "expiry": 1234567890 + "expiry": 1234567890, + "business_metrics": ["z"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_assume_role/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_assume_role/test-case.json index 8f59ddf5c61..aea0e7d55c4 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_assume_role/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_assume_role/test-case.json @@ -1,13 +1,14 @@ { "name": "imds-token-fail", - "docs": "acquires credentials via IMDS and uses them to assume a role", + "docs": "acquires credentials from a profile with IMDS as a named provider and uses them to assume a role", "result": { "Ok": { "access_key_id": "ASIARCORRECT", "secret_access_key": "secretkeycorrect", "session_token": "tokencorrect", "expiry": 1632249686, - "accountId": "130633740322" + "accountId": "130633740322", + "business_metrics": ["p","0", "i"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_config_with_no_creds/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_config_with_no_creds/test-case.json index 73a9fe8bb21..988fb6690a4 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_config_with_no_creds/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_config_with_no_creds/test-case.json @@ -6,7 +6,8 @@ "access_key_id": "ASIARTEST", "secret_access_key": "testsecret", "session_token": "testtoken", - "expiry": 1632197813 + "expiry": 1632197813, + "business_metrics": ["0"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_retries/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_retries/test-case.json index b8048ebbf63..c5c57a67115 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_retries/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_retries/test-case.json @@ -6,7 +6,8 @@ "access_key_id": "ASIARTEST", "secret_access_key": "testsecret", "session_token": "testtoken", - "expiry": 1632270906 + "expiry": 1632270906, + "business_metrics": ["0"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_success/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_success/test-case.json index f951f70eea8..63ebc8b3118 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_success/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/imds_default_chain_success/test-case.json @@ -6,7 +6,8 @@ "access_key_id": "ASIARTEST", "secret_access_key": "testsecret", "session_token": "testtoken", - "expiry": 1632197813 + "expiry": 1632197813, + "business_metrics": ["0"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/legacy_sso_role/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/legacy_sso_role/test-case.json index 2406302d49c..083df326798 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/legacy_sso_role/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/legacy_sso_role/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "secretkeycorrect", "session_token": "tokencorrect", "expiry": 1641240833, - "accountId": "123456789" + "accountId": "123456789", + "business_metrics": ["t","u"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_environment/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_environment/test-case.json index 97751f5ef4e..5763fe0e8dc 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_environment/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_environment/test-case.json @@ -4,7 +4,8 @@ "result": { "Ok": { "access_key_id": "correct_key", - "secret_access_key": "correct_secret" + "secret_access_key": "correct_secret", + "business_metrics": ["g"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_system_properties/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_system_properties/test-case.json index 8a0f7984a63..5a0dffa6423 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_system_properties/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/prefer_system_properties/test-case.json @@ -4,7 +4,8 @@ "result": { "Ok": { "access_key_id": "correct_key", - "secret_access_key": "correct_secret" + "secret_access_key": "correct_secret", + "business_metrics": ["f"] } } } \ No newline at end of file diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_name/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_name/test-case.json index f7a1a28b8b8..5d4250229df 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_name/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_name/test-case.json @@ -4,7 +4,8 @@ "result": { "Ok": { "access_key_id": "correct_key", - "secret_access_key": "correct_secret" + "secret_access_key": "correct_secret", + "business_metrics": ["n"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_static_keys/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_static_keys/test-case.json index c56a02bac65..10d9c849225 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_static_keys/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/profile_static_keys/test-case.json @@ -4,7 +4,8 @@ "result": { "Ok": { "access_key_id": "correct_key", - "secret_access_key": "correct_secret" + "secret_access_key": "correct_secret", + "business_metrics": ["n"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/retry_on_error/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/retry_on_error/test-case.json index 0eccf448608..dc73d64080d 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/retry_on_error/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/retry_on_error/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "TESTSECRETKEY", "session_token": "TESTSESSIONTOKEN", "expiry": 1628193482, - "accountId": "123456789012" + "accountId": "123456789012", + "business_metrics": ["o","n", "i"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/sso_session/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/sso_session/test-case.json index fdb622b1ff8..462c1fae0ba 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/sso_session/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/sso_session/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "secretkeycorrect", "session_token": "tokencorrect", "expiry": 1641240833, - "accountId": "123456789" + "accountId": "123456789", + "business_metrics": ["r","s"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_env/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_env/test-case.json index 511e780ad17..ce71bbc5209 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_env/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_env/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "SECRETKEYTEST", "session_token": "SESSIONTOKEN_TEST", "expiry": 1629147173, - "accountId": "123456789012" + "accountId": "123456789012", + "business_metrics": ["h", "k"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_profile/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_profile/test-case.json index 3d18b175471..939cdf9afcd 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_profile/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_profile/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "TESTSECRET", "session_token": "TESTSESSIONTOKEN", "expiry": 1629233704, - "accountId": "123456789012" + "accountId": "123456789012", + "business_metrics": ["q", "k"] } } } diff --git a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_source_profile/test-case.json b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_source_profile/test-case.json index b1b0ea1e38b..22ade3a6fea 100644 --- a/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_source_profile/test-case.json +++ b/aws-runtime/aws-config/common/test-resources/default-provider-chain/web_identity_token_source_profile/test-case.json @@ -7,7 +7,8 @@ "secret_access_key": "TESTSECRETKEY", "session_token": "TESTSESSIONTOKEN", "expiry": 1628193482, - "accountId": "123456789012" + "accountId": "123456789012", + "business_metrics": ["o","q", "k", "i"] } } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProviderTest.kt index 4aeb36c0097..8401b507b59 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EcsCredentialsProviderTest.kt @@ -7,6 +7,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException import aws.smithy.kotlin.runtime.http.Headers @@ -46,7 +48,7 @@ class EcsCredentialsProviderTest { "test-token", expectedExpiration, "EcsContainer", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_HTTP) private fun ecsResponse(accountId: String? = null): HttpResponse { val payload = buildJsonObject { @@ -576,7 +578,7 @@ class EcsCredentialsProviderTest { expectedExpiration, "EcsContainer", "12345", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_HTTP) assertEquals(expected, actual) engine.assertRequests() } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProviderTest.kt index 09ac8d1392c..23605206185 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/EnvironmentCredentialsProviderTest.kt @@ -7,6 +7,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.client.AwsClientOption import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.collections.attributesOf import io.kotest.matchers.string.shouldContain @@ -25,7 +27,15 @@ class EnvironmentCredentialsProviderTest { AwsSdkSetting.AwsSecretAccessKey.envVar to "def", AwsSdkSetting.AwsSessionToken.envVar to "ghi", ) - assertEquals(provider.resolve(), Credentials("abc", "def", "ghi", providerName = "Environment")) + assertEquals( + provider.resolve(), + Credentials( + "abc", + "def", + "ghi", + providerName = "Environment", + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS), + ) } @Test @@ -34,7 +44,15 @@ class EnvironmentCredentialsProviderTest { AwsSdkSetting.AwsAccessKeyId.envVar to "abc", AwsSdkSetting.AwsSecretAccessKey.envVar to "def", ) - assertEquals(provider.resolve(), Credentials("abc", "def", null, providerName = "Environment")) + assertEquals( + provider.resolve(), + Credentials( + "abc", + "def", + null, + providerName = "Environment", + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS), + ) } @Test @@ -85,7 +103,7 @@ class EnvironmentCredentialsProviderTest { "def", providerName = "Environment", attributes = attributesOf { AwsClientOption.AccountId to "12345" }, - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS) assertEquals(expected, actual) } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt index 26e8e8f517b..90a30e9e9c8 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt @@ -7,6 +7,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting import aws.sdk.kotlin.runtime.config.imds.* import aws.sdk.kotlin.runtime.config.imds.DEFAULT_TOKEN_TTL_SECONDS +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException import aws.smithy.kotlin.runtime.http.* @@ -126,7 +128,7 @@ class ImdsCredentialsProviderTest { "IQote///test0", expiration0, "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) assertEquals(expected0, actual0) testClock.advance(1.seconds) @@ -138,7 +140,7 @@ class ImdsCredentialsProviderTest { "IQote///test1", expiration1, "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) assertEquals(expected1, actual1) connection.assertRequests() @@ -195,7 +197,7 @@ class ImdsCredentialsProviderTest { "IQote///test", expiration, "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) assertEquals(expected, actual) connection.assertRequests() @@ -334,7 +336,7 @@ class ImdsCredentialsProviderTest { sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) assertEquals(expected, actual) @@ -402,7 +404,7 @@ class ImdsCredentialsProviderTest { sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", @@ -519,7 +521,7 @@ class ImdsCredentialsProviderTest { sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileChainTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileChainTest.kt index 75e4301ace2..90f1268ad8a 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileChainTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileChainTest.kt @@ -8,11 +8,14 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.profile.LeafProvider import aws.sdk.kotlin.runtime.auth.credentials.profile.ProfileChain import aws.sdk.kotlin.runtime.auth.credentials.profile.RoleArn +import aws.sdk.kotlin.runtime.auth.credentials.profile.RoleArnSource +import aws.sdk.kotlin.runtime.client.AwsClientOption import aws.sdk.kotlin.runtime.config.profile.AwsConfigurationSource import aws.sdk.kotlin.runtime.config.profile.FileType import aws.sdk.kotlin.runtime.config.profile.parse import aws.sdk.kotlin.runtime.config.profile.toSharedConfig import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.collections.attributesOf import aws.smithy.kotlin.runtime.telemetry.logging.Logger import kotlin.test.Test import kotlin.test.assertEquals @@ -35,7 +38,7 @@ class ProfileChainTest { private fun chain(leaf: LeafProvider, vararg roles: RoleArn): TestOutput = TestOutput.Chain(ProfileChain(leaf, roles.toList())) - private val tests = listOf( + private val kitchenSinkTests = listOf( TestCase( "basic role arn backed by static credentials", """ @@ -49,11 +52,11 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("abc123", "def456")), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( - "ignore explicit credentials when source profile is specified", + "ignore source profile when explicit credentials are specified", """ [profile A] aws_access_key_id = abc123 @@ -66,8 +69,7 @@ class ProfileChainTest { aws_secret_access_key = jkl123 """, chain( - LeafProvider.AccessKey(Credentials("ghi890", "jkl123")), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + LeafProvider.AccessKey(Credentials("abc123", "def456")), ), ), TestCase( @@ -84,7 +86,7 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("abc123", "def456")), - RoleArn("arn:aws:iam::123456789:role/RoleA", "my_session_name"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE, "my_session_name"), ), ), TestCase( @@ -101,11 +103,11 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("abc123", "def456")), - RoleArn("arn:aws:iam::123456789:role/RoleA", externalId = "my_external_id"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE, externalId = "my_external_id"), ), ), TestCase( - "self referential profile (first load base creds, then use for the role)", + "self referential profile (load base credentials, ignore role)", """ [profile A] aws_access_key_id = abc123 @@ -115,7 +117,6 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("abc123", "def456")), - RoleArn("arn:aws:iam::123456789:role/RoleA"), ), ), TestCase( @@ -127,7 +128,7 @@ class ProfileChainTest { """, chain( LeafProvider.NamedSource("Ec2InstanceMetadata"), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.CREDENTIALS_SOURCE), ), ), TestCase( @@ -153,7 +154,7 @@ class ProfileChainTest { TestOutput.Error("profile (A) contained both `source_profile` and `credential_source`. Only one or the other can be defined."), ), TestCase( - "partial credentials error (missing secret)", + "partial static credentials (missing secret) in source profile leads to credentials not being found", """ [profile A] role_arn = arn:foo @@ -162,10 +163,10 @@ class ProfileChainTest { [profile B] aws_access_key_id = abc123 """, - TestOutput.Error("profile (B) missing `aws_secret_access_key`"), + TestOutput.Error("profile (B) did not contain credential information"), ), TestCase( - "partial credentials error (missing access key)", + "partial static credentials (missing access key) in source profile leads to credentials not being found", """ [profile A] role_arn = arn:foo @@ -174,7 +175,7 @@ class ProfileChainTest { [profile B] aws_secret_access_key = abc123 """, - TestOutput.Error("profile (B) missing `aws_access_key_id`"), + TestOutput.Error("profile (B) did not contain credential information"), ), TestCase( "missing credentials error (empty source profile)", @@ -221,8 +222,8 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("mno456", "pqr789")), - RoleArn("arn:aws:iam::123456789:role/RoleB"), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleB", RoleArnSource.SOURCE_PROFILE), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( @@ -230,8 +231,7 @@ class ProfileChainTest { """ [profile A] role_arn = arn:aws:iam::123456789:role/RoleA - aws_access_key_id = bug_if_returned - aws_secret_access_key = bug_if_returned + web_identity_token_file = bug/if/returned source_profile = B [profile B] @@ -246,7 +246,7 @@ class ProfileChainTest { """, chain( LeafProvider.AccessKey(Credentials("profile_b_key", "profile_b_secret")), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( @@ -263,12 +263,11 @@ class ProfileChainTest { TestOutput.Error("profile formed an infinite loop: A -> B -> A"), ), TestCase( - "infinite loop with static credentials", + "infinite loop with web identity token", """ [profile A] role_arn = arn:aws:iam::123456789:role/RoleA - aws_access_key_id = bug_if_returned - aws_secret_access_key = bug_if_returned + web_identity_token_file=bug/if/returned source_profile = B [profile B] @@ -322,7 +321,7 @@ class ProfileChainTest { """, chain( LeafProvider.WebIdentityTokenRole("arn:aws:iam::123456789:role/RoleB", "/var/token.jwt", "some_session_name"), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( @@ -354,7 +353,7 @@ class ProfileChainTest { """, chain( LeafProvider.LegacySso("https://d-92671207e4.awsapps.com/start", "us-east-2", "1234567", "RoleA"), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( @@ -436,7 +435,7 @@ class ProfileChainTest { """, chain( LeafProvider.SsoSession("my-session", "https://d-92671207e4.awsapps.com/start", "us-east-2", "1234567", "RoleA"), - RoleArn("arn:aws:iam::123456789:role/RoleA"), + RoleArn("arn:aws:iam::123456789:role/RoleA", RoleArnSource.SOURCE_PROFILE), ), ), TestCase( @@ -497,9 +496,183 @@ class ProfileChainTest { ), ) - @Test - fun testProfileChainResolution() { - tests.forEachIndexed { idx, test -> + private val precedenceTests = listOf( + TestCase( + "static credentials precedence test", + """ + [profile A] + aws_access_key_id = 1 + aws_secret_access_key = 2 + aws_session_token = 3 + aws_account_id = 4 + source_profile = B + role_arn = some-arn + web_identity_token_file = /some/path + sso_session = dev\nsso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + + [profile B] + aws_access_key_id = 0 + aws_secret_access_key = 0 + aws_session_token = 0 + aws_account_id = 0 + + [sso-session dev] + sso_region = us-west-2 + sso_start_url = https://some.url + """, + chain( + LeafProvider.AccessKey( + Credentials( + "1", + "2", + "3", + attributes = attributesOf { + AwsClientOption.AccountId to "4" + }, + ), + ), + ), + ), + TestCase( + "assume role with source profile precedence test", + """ + [profile A] + source_profile = B + role_arn = some-arn + web_identity_token_file = /some/path + sso_session = dev + sso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + + [profile B] + aws_access_key_id = 1 + aws_secret_access_key = 2 + aws_session_token = 3 + aws_account_id = 4 + + [sso-session dev] + sso_region = us-west-2 + sso_start_url = https://some.url + """, + chain( + LeafProvider.AccessKey( + Credentials( + "1", + "2", + "3", + attributes = attributesOf { + AwsClientOption.AccountId to "4" + }, + ), + ), + RoleArn("some-arn", RoleArnSource.SOURCE_PROFILE), + ), + ), + TestCase( + "assume role with named provider / credential source precedence test", + """ + [profile A] + role_arn = some-arn + credential_source = Ec2InstanceMetadata + web_identity_token_file = /some/path + sso_session = dev + sso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + + [sso-session dev] + sso_region = us-west-2 + sso_start_url = https://some.url + """, + chain( + LeafProvider.NamedSource("Ec2InstanceMetadata"), + RoleArn("some-arn", RoleArnSource.CREDENTIALS_SOURCE), + ), + ), + TestCase( + "web identity token / STS precedence test", + """ + [profile A] + role_arn = some-arn + web_identity_token_file = /some/path + sso_session = dev + sso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + + [sso-session dev] + sso_region = us-west-2 + sso_start_url = https://some.url + """, + chain( + LeafProvider.WebIdentityTokenRole("some-arn", "/some/path"), + ), + ), + TestCase( + "SSO role precedence test", + """ + [profile A] + sso_session = dev + sso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + + [sso-session dev] + sso_region = us-west-2 + sso_start_url = https://some.url + """, + chain( + LeafProvider.SsoSession("dev", "https://some.url", "us-west-2", "12345678901", "role"), + ), + ), + TestCase( + "legacy SSO precedence test", + """ + [profile A] + sso_account_id = 12345678901 + sso_role_name = role + sso_region = us-west-2 + sso_start_url = https://some.url + credential_process = some/process + """, + chain( + LeafProvider.LegacySso("https://some.url", "us-west-2", "12345678901", "role"), + ), + ), + TestCase( + "process precedence test", + """ + [profile A] + credential_process = some/process + """, + chain( + LeafProvider.Process("some/process"), + ), + ), + TestCase( + "empty profile", + """ + [profile A] + """, + TestOutput.Error("profile (A) did not contain credential information"), + ), + ) + + private fun List.run() = + this.forEachIndexed { idx, test -> val profiles = parse(Logger.None, FileType.CONFIGURATION, test.profile.trimIndent()) val source = AwsConfigurationSource(test.activeProfile, "not-needed", "not-needed") val config = profiles.toSharedConfig(source) @@ -519,5 +692,10 @@ class ProfileChainTest { } } } + + @Test + fun testProfileChainResolution() { + kitchenSinkTests.run() + precedenceTests.run() } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProviderTest.kt index dd26b335337..d4f6eaf51c4 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProviderTest.kt @@ -7,12 +7,20 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials import aws.sdk.kotlin.runtime.client.AwsClientOption +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetrics +import aws.sdk.kotlin.runtime.util.testAttributes import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.copy import aws.smithy.kotlin.runtime.collections.attributesOf import aws.smithy.kotlin.runtime.httptest.TestConnection import aws.smithy.kotlin.runtime.httptest.buildTestConnection import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.util.TestPlatformProvider +import io.mockk.coEvery +import io.mockk.mockkStatic import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -38,6 +46,7 @@ class ProfileCredentialsProviderTest { ) val actual = provider.resolve() val expected = Credentials("AKID-Default", "Default-Secret") + .withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE) assertEquals(expected, actual) } @@ -66,6 +75,7 @@ class ProfileCredentialsProviderTest { ) val actual = provider.resolve() val expected = Credentials("AKID-Profile", "Profile-Secret") + .withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE) assertEquals(expected, actual) } @@ -96,6 +106,7 @@ class ProfileCredentialsProviderTest { ) val actual = provider.resolve() val expected = Credentials("AKID-Profile", "Profile-Secret") + .withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE) assertEquals(expected, actual) } @@ -131,7 +142,15 @@ class ProfileCredentialsProviderTest { httpClient = testEngine, ) val actual = provider.resolve() - assertEquals(StsTestUtils.CREDENTIALS, actual) + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_SOURCE_PROFILE, + AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) testEngine.assertRequests() val req = testEngine.requests().first() @@ -316,6 +335,99 @@ class ProfileCredentialsProviderTest { ) val actual = provider.resolve() val expected = credentials("AKID-Default", "Default-Secret", accountId = "12345") + .withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE) + assertEquals(expected, actual) + } + + @Test + fun assumeRoleWithNamedProviderBusinessMetrics() = runTest { + val testArn = "arn:aws:iam::1234567:role/test-role" + val testProvider = TestPlatformProvider( + env = mapOf( + "AWS_CONFIG_FILE" to "config", + "AWS_REGION" to "us-west-2", + "AWS_ACCESS_KEY_ID" to "1", + "AWS_SECRET_ACCESS_KEY" to "2", + ), + fs = mapOf( + "config" to """ + [default] + role_arn = $testArn + credential_source = Environment + """.trimIndent(), + ), + ) + val testEngine = buildTestConnection { + expect(StsTestUtils.stsResponse(testArn)) + } + + val provider = ProfileCredentialsProvider( + platformProvider = testProvider, + httpClient = testEngine, + ) + + val actual = provider.resolve() + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_NAMED_PROVIDER, + AwsBusinessMetric.Credentials.CREDENTIALS_ENV_VARS, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) + } + + @Test + fun processBusinessMetrics() = runTest { + val testProvider = TestPlatformProvider( + env = mapOf( + "AWS_CONFIG_FILE" to "config", + ), + fs = mapOf( + "config" to """ + [default] + credential_process = awscreds-custom + """.trimIndent(), + "awscreds-custom" to "some-process", + ), + ) + val testEngine = TestConnection() + val provider = ProfileCredentialsProvider( + platformProvider = testProvider, + httpClient = testEngine, + ) + + mockkStatic(::executeCommand) + coEvery { executeCommand(any(), any(), any(), any(), any()) }.returns( + Pair( + 0, + """ + { + "Version": 1, + "AccessKeyId": "AKID-Default", + "SecretAccessKey": "Default-Secret", + "SessionToken": "SessionToken", + "Expiration" : "2019-05-29T00:21:43Z" + } + """.trimIndent(), + ), + ) + + val actual = provider.resolve() + val expected = credentials( + "AKID-Default", + "Default-Secret", + "SessionToken", + Instant.fromIso8601("2019-05-29T00:21:43Z"), + "Process", + ).withBusinessMetrics( + setOf( + AwsBusinessMetric.Credentials.CREDENTIALS_PROFILE_PROCESS, + AwsBusinessMetric.Credentials.CREDENTIALS_PROCESS, + ), + ) + assertEquals(expected, actual) } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProviderTest.kt index 37f2852ea7a..af0bf6acb45 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SsoCredentialsProviderTest.kt @@ -6,6 +6,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.HttpStatusCode @@ -201,7 +203,14 @@ class SsoCredentialsProviderTest { ) val actual = provider.resolve() - val expected = credentials("AKID", "secret", "session-token", expectedExpiration, "SSO", "123456789") + val expected = credentials( + "AKID", + "secret", + "session-token", + expectedExpiration, + "SSO", + "123456789", + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_SSO_LEGACY) assertEquals(expected, actual) } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProviderTest.kt index c7b5d13815d..520ecbd378d 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsAssumeRoleCredentialsProviderTest.kt @@ -6,7 +6,10 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.RegionDisabledException +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.util.testAttributes import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException +import aws.smithy.kotlin.runtime.auth.awscredentials.copy import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.HttpStatusCode @@ -49,7 +52,13 @@ class StsAssumeRoleCredentialsProviderTest { ) val actual = provider.resolve() - assertEquals(StsTestUtils.CREDENTIALS, actual) + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) testEngine.assertRequests(CallAsserter.MatchingBodies) } @@ -87,7 +96,13 @@ class StsAssumeRoleCredentialsProviderTest { ) val actual = provider.resolve() - assertEquals(StsTestUtils.CREDENTIALS, actual) + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) testEngine.assertRequests(CallAsserter.MatchingBodies) } @@ -163,7 +178,13 @@ class StsAssumeRoleCredentialsProviderTest { ) val actual = provider.resolve() - assertEquals(StsTestUtils.CREDENTIALS, actual) + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) val req = testEngine.requests().first() assertEquals(Host.Domain("sts.amazonaws.com"), req.actual.url.host) } @@ -182,7 +203,13 @@ class StsAssumeRoleCredentialsProviderTest { ) val actual = provider.resolve() - assertEquals(StsTestUtils.CREDENTIALS, actual) + val expected = StsTestUtils.CREDENTIALS.copy( + attributes = testAttributes( + StsTestUtils.CREDENTIALS.attributes, + AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE, + ), + ) + assertEquals(expected, actual) val req = testEngine.requests().first() assertEquals(Host.Domain("sts.us-west-2.amazonaws.com"), req.actual.url.host) } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProviderTest.kt index 2194df2593a..7f4df827213 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/StsWebIdentityCredentialsProviderTest.kt @@ -6,6 +6,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpBody @@ -34,7 +36,7 @@ private val CREDENTIALS = credentials( StsTestUtils.EPOCH + 15.minutes, "WebIdentityToken", "1234567", -) +).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_STS_ASSUME_ROLE_WEB_ID) class StsWebIdentityCredentialsProviderTest { // see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProviderTest.kt index 4c16609885f..16d287f0e80 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/SystemPropertyCredentialsProviderTest.kt @@ -6,6 +6,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.util.testAttributes import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import io.kotest.matchers.string.shouldContain import kotlinx.coroutines.test.runTest @@ -23,7 +25,18 @@ class SystemPropertyCredentialsProviderTest { AwsSdkSetting.AwsSecretAccessKey.sysProp to "def", AwsSdkSetting.AwsSessionToken.sysProp to "ghi", ) - assertEquals(provider.resolve(), Credentials("abc", "def", "ghi", providerName = "SystemProperties")) + assertEquals( + provider.resolve(), + Credentials( + "abc", + "def", + "ghi", + providerName = "SystemProperties", + attributes = testAttributes( + AwsBusinessMetric.Credentials.CREDENTIALS_JVM_SYSTEM_PROPERTIES, + ), + ), + ) } @Test @@ -32,7 +45,18 @@ class SystemPropertyCredentialsProviderTest { AwsSdkSetting.AwsAccessKeyId.sysProp to "abc", AwsSdkSetting.AwsSecretAccessKey.sysProp to "def", ) - assertEquals(provider.resolve(), Credentials("abc", "def", null, providerName = "SystemProperties")) + assertEquals( + provider.resolve(), + Credentials( + "abc", + "def", + null, + providerName = "SystemProperties", + attributes = testAttributes( + AwsBusinessMetric.Credentials.CREDENTIALS_JVM_SYSTEM_PROPERTIES, + ), + ), + ) } @Test diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/util/AwsBusinessMetricsTestUtils.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/util/AwsBusinessMetricsTestUtils.kt new file mode 100644 index 00000000000..71467475b34 --- /dev/null +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/util/AwsBusinessMetricsTestUtils.kt @@ -0,0 +1,36 @@ +package aws.sdk.kotlin.runtime.util + +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.collections.mutableAttributes +import aws.smithy.kotlin.runtime.collections.toMutableAttributes + +/** + * [Attributes] and any [BusinessMetric] that should be included. + */ +internal fun testAttributes(attributes: Attributes? = null, vararg metrics: BusinessMetric): Attributes { + val testAttributes = attributes?.toMutableAttributes() ?: mutableAttributes() + metrics.forEach { metric -> + testAttributes.emitBusinessMetric(metric) + } + return testAttributes +} + +/** + * [Attributes] that only contain the specified [BusinessMetric]. + */ +internal fun testAttributes(vararg metrics: BusinessMetric): Attributes { + val testAttributes = mutableAttributes() + metrics.forEach { metric -> + testAttributes.emitBusinessMetric(metric) + } + return testAttributes +} + +/** + * Converts a [String] into an [AwsBusinessMetric.Credentials] if the identifier matches + */ +internal fun String.toAwsCredentialsBusinessMetric(): BusinessMetric = + AwsBusinessMetric.Credentials.entries.find { it.identifier == this } ?: throw Exception("String '$this' is not an AWS business metric") diff --git a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProviderTest.kt b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProviderTest.kt index 93bddd342f4..d56b63cd225 100644 --- a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProviderTest.kt @@ -6,6 +6,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetrics +import aws.sdk.kotlin.runtime.util.toAwsCredentialsBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.copy import aws.smithy.kotlin.runtime.httptest.TestConnection @@ -17,10 +19,7 @@ import aws.smithy.kotlin.runtime.util.PlatformProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.* import java.io.File import java.nio.file.Paths import kotlin.test.Test @@ -96,14 +95,17 @@ class DefaultChainCredentialsProviderTest { return when { "Ok" in result -> { val o = checkNotNull(result["Ok"]).jsonObject - val creds = credentials( + val expectedBusinessMetrics = o["business_metrics"]?.jsonArray?.map { it.jsonPrimitive.content }?.toMutableSet() ?: mutableSetOf() + val expectedCreds = credentials( checkNotNull(o["access_key_id"]).jsonPrimitive.content, checkNotNull(o["secret_access_key"]).jsonPrimitive.content, o["session_token"]?.jsonPrimitive?.content, o["expiry"]?.jsonPrimitive?.longOrNull?.let { Instant.fromEpochSeconds(it) }, accountId = o["accountId"]?.jsonPrimitive?.content, + ).withBusinessMetrics( + expectedBusinessMetrics.map { it.toAwsCredentialsBusinessMetric() }.toSet(), ) - Ok(name, docs, creds) + Ok(name, docs, expectedCreds) } "ErrorContains" in result -> ErrorContains(name, docs, checkNotNull(result["ErrorContains"]).jsonPrimitive.content) else -> error("unrecognized result object: $result") diff --git a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProviderTest.kt b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProviderTest.kt index 6752529dd13..da740a32c74 100644 --- a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/auth/credentials/ProcessCredentialsProviderTest.kt @@ -5,6 +5,8 @@ package aws.sdk.kotlin.runtime.auth.credentials import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException import aws.smithy.kotlin.runtime.time.Instant @@ -41,7 +43,7 @@ class ProcessCredentialsProviderTest { sessionToken = "SessionToken", expiration = Instant.fromEpochSeconds(1665705600), providerName = "Process", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROCESS) val processCredentialsProvider = ProcessCredentialsProvider("anyString") val actualCredentials = processCredentialsProvider.resolve() @@ -71,7 +73,7 @@ class ProcessCredentialsProviderTest { sessionToken = "SessionToken", expiration = Instant.MAX_VALUE, providerName = "Process", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROCESS) val processCredentialsProvider = ProcessCredentialsProvider("anyString") val actualCredentials = processCredentialsProvider.resolve() @@ -164,7 +166,7 @@ class ProcessCredentialsProviderTest { expiration = Instant.fromEpochSeconds(1665705600), providerName = "Process", accountId = "12345", - ) + ).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_PROCESS) val processCredentialsProvider = ProcessCredentialsProvider("anyString") val actualCredentials = processCredentialsProvider.resolve() diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index 425d6c7ac51..d677ed9a9dc 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -173,7 +173,7 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor : public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -196,7 +196,45 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInter public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public final class aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getIdentifier ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric; + public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric; +} + +public final class aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field CREDENTIALS_CODE Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_ENV_VARS Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_HTTP Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_IMDS Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_JVM_SYSTEM_PROPERTIES Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROCESS Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_NAMED_PROVIDER Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_PROCESS Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_SOURCE_PROFILE Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_SSO Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_SSO_LEGACY Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_SSO Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_SSO_LEGACY Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_STS_ASSUME_ROLE Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static final field CREDENTIALS_STS_ASSUME_ROLE_WEB_ID Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getIdentifier ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; + public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetric$Credentials; +} + +public final class aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetricsUtilsKt { + public static final fun withBusinessMetric (Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials;Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric;)Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials; + public static final fun withBusinessMetrics (Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials;Ljava/util/Set;)Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials; +} + +public final class aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/BusinessMetricsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetricsUtils.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetricsUtils.kt new file mode 100644 index 00000000000..cf3d8586b3e --- /dev/null +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/AwsBusinessMetricsUtils.kt @@ -0,0 +1,98 @@ +package aws.sdk.kotlin.runtime.http.interceptors.businessmetrics + +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.copy +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.collections.MutableAttributes +import aws.smithy.kotlin.runtime.collections.toMutableAttributes + +/** + * Makes sure the metrics do not exceed the maximum size and truncates them if so. + */ +internal fun formatMetrics(metrics: MutableSet): String { + if (metrics.isEmpty()) return "" + val metricsString = metrics.joinToString(",", "m/") { it.identifier } + val metricsByteArray = metricsString.encodeToByteArray() + + if (metricsByteArray.size <= BUSINESS_METRICS_MAX_LENGTH) return metricsString + + val lastCommaIndex = metricsByteArray + .sliceArray(0 until 1024) + .indexOfLast { it == ','.code.toByte() } + .takeIf { it != -1 } + + lastCommaIndex?.let { + return metricsByteArray.decodeToString( + 0, + lastCommaIndex, + true, + ) + } + + throw IllegalStateException("Business metrics are incorrectly formatted: $metricsString") +} + +/** + * AWS SDK specific business metrics + */ +@InternalApi +public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric { + S3_EXPRESS_BUCKET("J"), + ; + + @InternalApi + public enum class Credentials(public override val identifier: String) : BusinessMetric { + 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_WEB_ID("k"), + 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"), + } +} + +/** + * Emits a business metric into [Credentials.attributes] + * @param metric The [BusinessMetric] to be emitted. + */ +@InternalApi +public fun Credentials.withBusinessMetric(metric: BusinessMetric): Credentials = + when (val credentialsAttributes = this.attributes) { + is MutableAttributes -> { + credentialsAttributes.emitBusinessMetric(metric) + this + } + else -> { + val newCredentialsAttributes = credentialsAttributes.toMutableAttributes() + newCredentialsAttributes.emitBusinessMetric(metric) + this.copy(attributes = newCredentialsAttributes) + } + } + +/** + * Emits business metrics into [Credentials.attributes] + * @param metrics The [BusinessMetric]s to be emitted. + */ +@InternalApi +public fun Credentials.withBusinessMetrics(metrics: Set): Credentials { + var credentials = this + metrics.forEach { metric -> + credentials = this.withBusinessMetric(metric) + } + return credentials +} diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/BusinessMetricsInterceptor.kt new file mode 100644 index 00000000000..43bed2aecf1 --- /dev/null +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/businessmetrics/BusinessMetricsInterceptor.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.runtime.http.interceptors.businessmetrics + +import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder + +/** + * Appends business metrics to the `User-Agent` header. + */ +public class BusinessMetricsInterceptor : HttpInterceptor { + override suspend fun modifyBeforeTransmit(context: ProtocolRequestInterceptorContext): HttpRequest { + context.executionContext.getOrNull(BusinessMetrics)?.let { metrics -> + val metricsString = formatMetrics(metrics) + val currentUserAgentHeader = context.protocolRequest.headers[USER_AGENT] + val modifiedRequest = context.protocolRequest.toBuilder() + + modifiedRequest.headers[USER_AGENT] = currentUserAgentHeader + metricsString + + return modifiedRequest.build() + } + return context.protocolRequest + } +} diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt index d1d47b22237..8b5038e61db 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -5,6 +5,8 @@ package aws.sdk.kotlin.runtime.http.interceptors import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric +import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.BusinessMetricsInterceptor import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric @@ -81,7 +83,6 @@ class BusinessMetricsInterceptorTest { @Test fun businessMetricsMaxLength() = runTest { val executionContext = ExecutionContext() - executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf() for (i in 0..BUSINESS_METRICS_MAX_LENGTH) { executionContext.emitBusinessMetric( diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index b4debfc4bd8..2246bdd13f7 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -60,8 +60,11 @@ object AwsRuntimeTypes { object Interceptors : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP, "interceptors") { val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor") val UnsupportedSigningAlgorithmInterceptor = symbol("UnsupportedSigningAlgorithmInterceptor") - val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") - val AwsBusinessMetric = symbol("AwsBusinessMetric") + + object BusinessMetrics : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP, "interceptors.businessmetrics") { + val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") + val AwsBusinessMetric = symbol("AwsBusinessMetric") + } } object Retries { diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/BusinessMetricsIntegration.kt new file mode 100644 index 00000000000..52f51b91382 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/BusinessMetricsIntegration.kt @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.codegen.businessmetrics + +import aws.sdk.kotlin.codegen.AwsRuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.shapes.OperationShape + +/** + * Renders the addition of the [BusinessMetricsInterceptor] + */ +class BusinessMetricsInterceptorIntegration : KotlinIntegration { + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + userAgentBusinessMetricsMiddleware + + private val userAgentBusinessMetricsMiddleware = object : ProtocolMiddleware { + override val name: String = "UserAgentBusinessMetrics" + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + AwsRuntimeTypes.Http.Interceptors.BusinessMetrics.BusinessMetricsInterceptor, + ) + } + } +} diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/CredentialsBusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/CredentialsBusinessMetricsIntegration.kt new file mode 100644 index 00000000000..162c12dbb09 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/CredentialsBusinessMetricsIntegration.kt @@ -0,0 +1,51 @@ +package aws.sdk.kotlin.codegen.businessmetrics + +import aws.sdk.kotlin.codegen.AwsRuntimeTypes +import software.amazon.smithy.aws.traits.auth.SigV4ATrait +import software.amazon.smithy.aws.traits.auth.SigV4Trait +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.withBlock +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.ServiceIndex +import software.amazon.smithy.model.shapes.OperationShape + +/** + * Renders the addition of some of the credentials related business metrics. + */ +class CredentialsBusinessMetricsIntegration : KotlinIntegration { + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean { + val serviceIndex = ServiceIndex.of(model) + val schemes = serviceIndex.getAuthSchemes(settings.service) + + return schemes.values.any { + it.javaClass == SigV4ATrait::class.java || it.javaClass == SigV4Trait::class.java + } + } + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + credentialsBusinessMetricsMiddleware + + private val credentialsBusinessMetricsMiddleware = object : ProtocolMiddleware { + override val name: String = "credentialsOverrideBusinessMetricsMiddleware" + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.withBlock( + "if (config.credentialsProvider is #T) {", + "}", + AwsRuntimeTypes.Config.Credentials.StaticCredentialsProvider, + ) { + write( + "op.context.#T(#T.Credentials.CREDENTIALS_CODE)", + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, + AwsRuntimeTypes.Http.Interceptors.BusinessMetrics.AwsBusinessMetric, + ) + } + } + } +} diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/EndpointBusinessMetricsIntegration.kt similarity index 57% rename from codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt rename to codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/EndpointBusinessMetricsIntegration.kt index 73575ddf075..5150c8d7167 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/businessmetrics/EndpointBusinessMetricsIntegration.kt @@ -1,27 +1,17 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.codegen +package aws.sdk.kotlin.codegen.businessmetrics -import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import aws.sdk.kotlin.codegen.AwsRuntimeTypes import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.integration.SectionWriter import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointBusinessMetrics -import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator -import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware -import software.amazon.smithy.model.shapes.OperationShape /** - * Renders the addition of the [BusinessMetricsInterceptor] and endpoint business metrics emitters + * Renders the addition of endpoint & endpoint adjacent business metrics. */ -class BusinessMetricsIntegration : KotlinIntegration { - override val order: Byte - get() = super.order - +class EndpointBusinessMetricsIntegration : KotlinIntegration { override val sectionWriters: List get() = listOf( SectionWriterBinding(EndpointBusinessMetrics, endpointBusinessMetricsSectionWriter), @@ -46,23 +36,8 @@ class BusinessMetricsIntegration : KotlinIntegration { AwsSigningAttributes, AwsSigningAttributes, RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, - AwsRuntimeTypes.Http.Interceptors.AwsBusinessMetric, + AwsRuntimeTypes.Http.Interceptors.BusinessMetrics.AwsBusinessMetric, ) writer.write("") } - - override fun customizeMiddleware( - ctx: ProtocolGenerator.GenerationContext, - resolved: List, - ): List = resolved + userAgentBusinessMetricsMiddleware - - private val userAgentBusinessMetricsMiddleware = object : ProtocolMiddleware { - override val name: String = "UserAgentBusinessMetrics" - override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { - writer.write( - "op.interceptors.add(#T())", - AwsRuntimeTypes.Http.Interceptors.BusinessMetricsInterceptor, - ) - } - } } diff --git a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index 60ccb728f63..7786616c0a6 100644 --- a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -41,7 +41,9 @@ aws.sdk.kotlin.codegen.customization.cloudfrontkeyvaluestore.BackfillSigV4ACusto aws.sdk.kotlin.codegen.customization.s3.express.SigV4S3ExpressAuthSchemeIntegration aws.sdk.kotlin.codegen.customization.s3.express.S3ExpressIntegration aws.sdk.kotlin.codegen.customization.s3.S3ExpiresIntegration -aws.sdk.kotlin.codegen.BusinessMetricsIntegration +aws.sdk.kotlin.codegen.businessmetrics.BusinessMetricsInterceptorIntegration +aws.sdk.kotlin.codegen.businessmetrics.CredentialsBusinessMetricsIntegration +aws.sdk.kotlin.codegen.businessmetrics.EndpointBusinessMetricsIntegration aws.sdk.kotlin.codegen.smoketests.AwsSmokeTestsRunnerGeneratorIntegration aws.sdk.kotlin.codegen.smoketests.SmokeTestsDenyListIntegration aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestSuccessHttpEngineIntegration diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39e0af010ea..2b51d27817e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ atomicfu-version = "0.25.0" binary-compatibility-validator-version = "0.16.3" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.3.29" -smithy-kotlin-codegen-version = "0.33.29" +smithy-kotlin-runtime-version = "1.3.30" +smithy-kotlin-codegen-version = "0.33.30" # codegen smithy-version = "1.51.0" diff --git a/services/s3/common/src/aws/sdk/kotlin/services/s3/express/DefaultS3ExpressCredentialsProvider.kt b/services/s3/common/src/aws/sdk/kotlin/services/s3/express/DefaultS3ExpressCredentialsProvider.kt index 3bfb627eccf..2edaca03d4f 100644 --- a/services/s3/common/src/aws/sdk/kotlin/services/s3/express/DefaultS3ExpressCredentialsProvider.kt +++ b/services/s3/common/src/aws/sdk/kotlin/services/s3/express/DefaultS3ExpressCredentialsProvider.kt @@ -10,6 +10,7 @@ import aws.sdk.kotlin.services.s3.S3Client import aws.smithy.kotlin.runtime.ExperimentalApi import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.simpleClassName import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.io.SdkManagedBase @@ -99,4 +100,6 @@ internal class DefaultS3ExpressCredentialsProvider( @OptIn(ExperimentalApi::class) internal val S3Client.logger get() = config.telemetryProvider.loggerProvider.getLogger() + + override fun toString(): String = this.simpleClassName } diff --git a/settings.gradle.kts b/settings.gradle.kts index fa43e4d2789..7f35e266c03 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -53,6 +53,7 @@ include(":hll:hll-mapping-core") include(":services") include(":tests") include(":tests:codegen:event-stream") +include(":tests:codegen:rules-engine") include(":tests:e2e-test-util") include(":tests:codegen:smoke-tests") include(":tests:codegen:smoke-tests:services") diff --git a/tests/codegen/rules-engine/build.gradle.kts b/tests/codegen/rules-engine/build.gradle.kts new file mode 100644 index 00000000000..ef35526b380 --- /dev/null +++ b/tests/codegen/rules-engine/build.gradle.kts @@ -0,0 +1,158 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import aws.sdk.kotlin.gradle.codegen.dsl.generateSmithyProjections +import aws.sdk.kotlin.gradle.codegen.dsl.smithyKotlinPlugin +import aws.sdk.kotlin.gradle.codegen.smithyKotlinProjectionSrcDir + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.aws.kotlin.repo.tools.smithybuild) +} + +description = "Smithy rules engine codegen integration test suite" + +data class Test( + val projectionName: String, + val protocolName: String, + val modelTemplate: File, +) { + val model: File + get() = layout.buildDirectory.file("$projectionName/model.smithy").get().asFile +} + +val tests = listOf( + Test("operationContextParams", "operationContextParams", file("operation-context-params.smithy")), +) + +fun fillInModel(output: File, protocolName: String, template: File) { + val input = template.readText() + val opTraits = when (protocolName) { + "restJson1", "restXml" -> """@http(method: "POST", uri: "/test-eventstream", code: 200)""" + else -> "" + } + val replaced = input + .replace("{protocol-name}", protocolName) + .replace("{op-traits}", opTraits) + + output.parentFile.mkdirs() + output.writeText(replaced) +} + +val testServiceShapeId = "aws.sdk.kotlin.test#TestService" +smithyBuild { + tests.forEach { test -> + + projections.register(test.projectionName) { + imports = listOf(test.model.absolutePath) + transforms = listOf( + """ + { + "name": "includeServices", + "args": { + "services": ["$testServiceShapeId"] + } + } + """, + ) + + smithyKotlinPlugin { + serviceShapeId = testServiceShapeId + packageName = "aws.sdk.kotlin.test.${test.projectionName.lowercase()}" + packageVersion = "1.0" + buildSettings { + generateFullProject = false + generateDefaultBuildFiles = false + optInAnnotations = listOf( + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + ) + } + } + } + } +} + +val codegen by configurations.getting +dependencies { + codegen(project(":codegen:aws-sdk-codegen")) + codegen(libs.smithy.cli) + codegen(libs.smithy.model) +} + +tasks.generateSmithyBuild { + doFirst { + tests.forEach { test -> fillInModel(test.model, test.protocolName, test.modelTemplate) } + } +} + +tasks.generateSmithyProjections { + doFirst { + // ensure the generated tests use the same version of the runtime as the aws aws-runtime + val smithyKotlinRuntimeVersion = libs.versions.smithy.kotlin.runtime.version.get() + System.setProperty("smithy.kotlin.codegen.clientRuntimeVersion", smithyKotlinRuntimeVersion) + } +} + +val optinAnnotations = listOf( + "kotlin.RequiresOptIn", + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", +) + +kotlin.sourceSets.all { + optinAnnotations.forEach { languageSettings.optIn(it) } +} + +kotlin.sourceSets.getByName("test") { + smithyBuild.projections.forEach { + kotlin.srcDir(smithyBuild.smithyKotlinProjectionSrcDir(it.name)) + } +} + +tasks.withType { + dependsOn(tasks.generateSmithyProjections) + // generated clients have quite a few warnings + kotlinOptions.allWarningsAsErrors = false +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} + +dependencies { + + implementation(libs.kotlinx.coroutines.core) + + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlin.test.junit5) + testImplementation(libs.kotlinx.coroutines.test) + + testImplementation(libs.smithy.kotlin.smithy.test) + testImplementation(libs.smithy.kotlin.aws.signing.default) + testImplementation(libs.smithy.kotlin.telemetry.api) + + // have to manually add all the dependencies of the generated client(s) + // doing it this way (as opposed to doing what we do for protocol-tests) allows + // the tests to work without a publish to maven-local step at the cost of maintaining + // this set of dependencies manually + // <-- BEGIN GENERATED DEPENDENCY LIST --> + implementation(libs.bundles.smithy.kotlin.service.client) + implementation(libs.smithy.kotlin.aws.event.stream) + implementation(project(":aws-runtime:aws-http")) + implementation(libs.smithy.kotlin.aws.json.protocols) + implementation(libs.smithy.kotlin.serde.json) + api(project(":aws-runtime:aws-config")) + api(project(":aws-runtime:aws-core")) + api(project(":aws-runtime:aws-endpoint")) + // <-- END GENERATED DEPENDENCY LIST --> +} diff --git a/tests/codegen/rules-engine/operation-context-params.smithy b/tests/codegen/rules-engine/operation-context-params.smithy new file mode 100644 index 00000000000..c63a2756e39 --- /dev/null +++ b/tests/codegen/rules-engine/operation-context-params.smithy @@ -0,0 +1,58 @@ +$version: "2.0" +namespace aws.sdk.kotlin.test + +use aws.protocols#awsJson1_0 +use smithy.rules#operationContextParams +use smithy.rules#endpointRuleSet +use aws.api#service + +@awsJson1_0 +@service(sdkId: "OperationContextParamsTest") +@endpointRuleSet( + version: "1.0", + parameters: { + "ObjectKeys": { + "type": "stringArray", + "documentation": "A string array.", + "required": true + } + }, + rules: [ + { + "type": "endpoint", + "conditions": [], + "endpoint": { + "url": "https://static.endpoint" + } + } + ] +) +service TestService { + operations: [DeleteObjects], + version: "1" +} + +@operationContextParams( + ObjectKeys: { + path: "Delete.Objects[*].[Key][]" + } +) +operation DeleteObjects { + input: DeleteObjectsRequest +} + +structure DeleteObjectsRequest { + Delete: Delete +} + +structure Delete { + Objects: ObjectIdentifierList +} + +list ObjectIdentifierList { + member: ObjectIdentifier +} + +structure ObjectIdentifier { + Key: String +}