
commonTest.aws.sdk.kotlin.runtime.auth.credentials.ProfileChainTest.kt Maven / Gradle / Ivy
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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.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.telemetry.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
class ProfileChainTest {
private sealed class TestOutput {
data class Chain(val chain: ProfileChain) : TestOutput()
data class Error(val message: String) : TestOutput()
}
private class TestCase(
val description: String,
val profile: String,
val output: TestOutput,
val activeProfile: String = "A",
)
private fun chain(leaf: LeafProvider, vararg roles: RoleArn): TestOutput =
TestOutput.Chain(ProfileChain(leaf, roles.toList()))
private val tests = listOf(
TestCase(
"basic role arn backed by static credentials",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = B
[profile B]
aws_access_key_id = abc123
aws_secret_access_key = def456
""",
chain(
LeafProvider.AccessKey(Credentials("abc123", "def456")),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"ignore explicit credentials when source profile is specified",
"""
[profile A]
aws_access_key_id = abc123
aws_secret_access_key = def456
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = B
[profile B]
aws_access_key_id = ghi890
aws_secret_access_key = jkl123
""",
chain(
LeafProvider.AccessKey(Credentials("ghi890", "jkl123")),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"load role_session_name for the AssumeRole provider",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
role_session_name = my_session_name
source_profile = B
[profile B]
aws_access_key_id = abc123
aws_secret_access_key = def456
""",
chain(
LeafProvider.AccessKey(Credentials("abc123", "def456")),
RoleArn("arn:aws:iam::123456789:role/RoleA", "my_session_name"),
),
),
TestCase(
"load external id for the AssumeRole provider",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
external_id = my_external_id
source_profile = B
[profile B]
aws_access_key_id = abc123
aws_secret_access_key = def456
""",
chain(
LeafProvider.AccessKey(Credentials("abc123", "def456")),
RoleArn("arn:aws:iam::123456789:role/RoleA", externalId = "my_external_id"),
),
),
TestCase(
"self referential profile (first load base creds, then use for the role)",
"""
[profile A]
aws_access_key_id = abc123
aws_secret_access_key = def456
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = A
""",
chain(
LeafProvider.AccessKey(Credentials("abc123", "def456")),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"Load credentials from a credential_source",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
credential_source = Ec2InstanceMetadata
""",
chain(
LeafProvider.NamedSource("Ec2InstanceMetadata"),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"role_arn without source source_profile",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
""",
TestOutput.Error("profile (A) must contain `source_profile` or `credential_source` but neither were defined"),
),
TestCase(
"source profile and credential source both present",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
credential_source = Environment
source_profile = B
[profile B]
aws_access_key_id = !23
aws_secret_access_key = 456
""",
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)",
"""
[profile A]
role_arn = arn:foo
source_profile = B
[profile B]
aws_access_key_id = abc123
""",
TestOutput.Error("profile (B) missing `aws_secret_access_key`"),
),
TestCase(
"partial credentials error (missing access key)",
"""
[profile A]
role_arn = arn:foo
source_profile = B
[profile B]
aws_secret_access_key = abc123
""",
TestOutput.Error("profile (B) missing `aws_access_key_id`"),
),
TestCase(
"missing credentials error (empty source profile)",
"""
[profile A]
role_arn = arn:foo
source_profile = B
[profile B]
""",
TestOutput.Error("profile (B) did not contain credential information"),
),
TestCase(
"profile only contains configuration",
"""
[profile A]
ec2_metadata_service_endpoint_mode = IPv6
""",
TestOutput.Error("profile (A) did not contain credential information"),
),
TestCase(
"missing source profile",
"""
[profile A]
role_arn = arn:foo
source_profile = B
""",
TestOutput.Error("could not find source profile B referenced from A"),
),
TestCase(
"multiple chained assume role profiles",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = B
[profile B]
role_arn = arn:aws:iam::123456789:role/RoleB
source_profile = C
[profile C]
aws_access_key_id = mno456
aws_secret_access_key = pqr789
""",
chain(
LeafProvider.AccessKey(Credentials("mno456", "pqr789")),
RoleArn("arn:aws:iam::123456789:role/RoleB"),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"chained assume role profiles with static credentials (ignore assume role when static credentials present)",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
aws_access_key_id = bug_if_returned
aws_secret_access_key = bug_if_returned
source_profile = B
[profile B]
role_arn = arn:aws:iam::123456789:role/RoleB
source_profile = C
aws_access_key_id = profile_b_key
aws_secret_access_key = profile_b_secret
[profile C]
aws_access_key_id = bug_if_returned
aws_secret_access_key = bug_if_returned
""",
chain(
LeafProvider.AccessKey(Credentials("profile_b_key", "profile_b_secret")),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"assume role profile infinite loop",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = B
[profile B]
role_arn = arn:aws:iam::123456789:role/RoleB
source_profile = A
""",
TestOutput.Error("profile formed an infinite loop: A -> B -> A"),
),
TestCase(
"infinite loop with static credentials",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
aws_access_key_id = bug_if_returned
aws_secret_access_key = bug_if_returned
source_profile = B
[profile B]
role_arn = arn:aws:iam::123456789:role/RoleB
source_profile = A
""",
TestOutput.Error("profile formed an infinite loop: A -> B -> A"),
),
TestCase(
"web identity role",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
web_identity_token_file = /var/token.jwt
""",
chain(
LeafProvider.WebIdentityTokenRole("arn:aws:iam::123456789:role/RoleA", "/var/token.jwt"),
),
),
TestCase(
"web identity role with session name",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
web_identity_token_file = /var/token.jwt
role_session_name = some_session_name
""",
chain(
LeafProvider.WebIdentityTokenRole("arn:aws:iam::123456789:role/RoleA", "/var/token.jwt", "some_session_name"),
),
),
TestCase(
"web identity missing role arn",
"""
[profile A]
web_identity_token_file = /var/token.jwt
""",
TestOutput.Error("profile (A) missing `role_arn`"),
),
TestCase(
"web identity token as source profile",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
source_profile = B
[profile B]
role_arn = arn:aws:iam::123456789:role/RoleB
web_identity_token_file = /var/token.jwt
role_session_name = some_session_name
""",
chain(
LeafProvider.WebIdentityTokenRole("arn:aws:iam::123456789:role/RoleB", "/var/token.jwt", "some_session_name"),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"single sign on",
"""
[profile A]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_region = us-east-2
sso_account_id = 1234567
sso_role_name = RoleA
""",
chain(
LeafProvider.LegacySso("https://d-92671207e4.awsapps.com/start", "us-east-2", "1234567", "RoleA"),
),
),
TestCase(
"single sign on as source profile",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
region = us-west-1
source_profile = B
[profile B]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_region = us-east-2
sso_account_id = 1234567
sso_role_name = RoleA
""",
chain(
LeafProvider.LegacySso("https://d-92671207e4.awsapps.com/start", "us-east-2", "1234567", "RoleA"),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"single sign on missing start url",
"""
[profile A]
sso_region = us-east-2
sso_account_id = 1234567
sso_role_name = RoleA
""",
TestOutput.Error("profile (A) missing `sso_start_url`"),
),
TestCase(
"single sign on missing sso region",
"""
[profile A]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_account_id = 1234567
sso_role_name = RoleA
""",
TestOutput.Error("profile (A) missing `sso_region`"),
),
TestCase(
"single sign on missing account id",
"""
[profile A]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_region = us-east-2
sso_role_name = RoleA
""",
TestOutput.Error("profile (A) missing `sso_account_id`"),
),
TestCase(
"single sign on missing role name",
"""
[profile A]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_region = us-east-2
sso_account_id = 1234567
""",
TestOutput.Error("profile (A) missing `sso_role_name`"),
),
TestCase(
"process credentials with absolute path",
"""
[profile A]
credential_process = /home/ec2-user/credential_provider.o
""",
chain(
LeafProvider.Process("/home/ec2-user/credential_provider.o"),
),
),
TestCase(
"process credentials resolved from PATH with additional arguments",
"""
[profile A]
credential_process = credential_provider --flag true
""",
chain(
LeafProvider.Process("credential_provider --flag true"),
),
),
TestCase(
"sso-session as source profile",
"""
[profile A]
role_arn = arn:aws:iam::123456789:role/RoleA
region = us-west-1
source_profile = B
[profile B]
sso_role_name = RoleA
sso_account_id = 1234567
sso_session = my-session
[sso-session my-session]
sso_region = us-east-2
sso_start_url = https://d-92671207e4.awsapps.com/start
""",
chain(
LeafProvider.SsoSession("my-session", "https://d-92671207e4.awsapps.com/start", "us-east-2", "1234567", "RoleA"),
RoleArn("arn:aws:iam::123456789:role/RoleA"),
),
),
TestCase(
"sso-session missing start url",
"""
[profile A]
sso_account_id = 1234567
sso_role_name = RoleA
sso_session = my-session
[sso-session my-session]
sso_region = us-east-2
""",
TestOutput.Error("sso-session (my-session) missing `sso_start_url`"),
),
TestCase(
"sso-session missing sso_region",
"""
[profile A]
sso_account_id = 1234567
sso_role_name = RoleA
sso_session = my-session
[sso-session my-session]
sso_start_url = https://d-92671207e4.awsapps.com/start
""",
TestOutput.Error("sso-session (my-session) missing `sso_region`"),
),
TestCase(
"sso-session and profile define differing sso_region",
"""
[profile A]
sso_account_id = 1234567
sso_role_name = RoleA
sso_region = us-east-1
sso_session = my-session
[sso-session my-session]
sso_start_url = https://d-92671207e4.awsapps.com/start
sso_region = us-west-2
""",
TestOutput.Error("sso-session (my-session) sso_region = `us-west-2` does not match profile (A) sso_region = `us-east-1`"),
),
TestCase(
"sso-session and profile define differing sso_start_url",
"""
[profile A]
sso_account_id = 1234567
sso_role_name = RoleA
sso_start_url = https://d-1
sso_session = my-session
[sso-session my-session]
sso_start_url = https://d-2
sso_region = us-west-2
""",
TestOutput.Error("sso-session (my-session) sso_start_url = `https://d-2` does not match profile (A) sso_start_url = `https://d-1`"),
),
)
@Test
fun testProfileChainResolution() {
tests.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)
val result = runCatching { ProfileChain.resolve(config) }
when {
result.isFailure && test.output is TestOutput.Chain -> fail("[idx=$idx, desc=${test.description}]: expected success but chain failed to resolve: $result")
result.isSuccess && test.output is TestOutput.Error -> fail("[idx=$idx, desc=${test.description}]: expected failure but chain resolved successfully: $result")
result.isFailure && test.output is TestOutput.Error -> {
val ex = result.exceptionOrNull() ?: fail("[idx=$idx, desc=${test.description}]: expected exception")
assertEquals(test.output.message, ex.message, "[idx=$idx, desc=${test.description}]: expected exception")
}
else -> {
val actual = result.getOrThrow()
val expected = test.output as TestOutput.Chain
assertEquals(expected.chain, actual, "[idx=$idx, desc=${test.description}]: chains not equal")
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy