All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonTest.aws.sdk.kotlin.runtime.auth.credentials.ImdsCredentialsProviderTest.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.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.imds.*
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException
import aws.smithy.kotlin.runtime.http.Headers
import aws.smithy.kotlin.runtime.http.HttpBody
import aws.smithy.kotlin.runtime.http.HttpMethod
import aws.smithy.kotlin.runtime.http.HttpStatusCode
import aws.smithy.kotlin.runtime.http.content.ByteArrayContent
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.response.HttpCall
import aws.smithy.kotlin.runtime.http.response.HttpResponse
import aws.smithy.kotlin.runtime.httptest.TestEngine
import aws.smithy.kotlin.runtime.httptest.buildTestConnection
import aws.smithy.kotlin.runtime.net.Host
import aws.smithy.kotlin.runtime.net.Scheme
import aws.smithy.kotlin.runtime.net.Url
import aws.smithy.kotlin.runtime.operation.ExecutionContext
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.ManualClock
import aws.smithy.kotlin.runtime.time.epochMilliseconds
import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds
import aws.smithy.kotlin.runtime.util.TestPlatformProvider
import io.kotest.matchers.string.shouldContain
import io.mockk.coVerify
import io.mockk.spyk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.assertNotEquals
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

private val ec2MetadataDisabledPlatform = TestPlatformProvider(
    env = mapOf(AwsSdkSetting.AwsEc2MetadataDisabled.envVar to "true"),
)
private val ec2MetadataEnabledPlatform = TestPlatformProvider()

@OptIn(ExperimentalCoroutinesApi::class)
class ImdsCredentialsProviderTest {

    @Test
    fun testImdsDisabled() = runTest {
        val platform = ec2MetadataDisabledPlatform
        val provider = ImdsCredentialsProvider(platformProvider = platform)
        assertFailsWith {
            provider.resolve()
        }.message.shouldContain("AWS EC2 metadata is explicitly disabled; credentials not loaded")
    }

    @Test
    fun testSuccess() = runTest {
        val testClock = ManualClock(Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds))
        val expiration0 = Instant.fromEpochMilliseconds(testClock.now().epochMilliseconds)
        val expiration1 = expiration0 + 2.seconds

        val connection = buildTestConnection {
            expect(
                tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS),
                tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"),
            )
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials", "TOKEN_A"),
                imdsResponse("imds-test-role"),
            )
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"),
                imdsResponse(
                    """
                    {
                        "Code" : "Success",
                        "LastUpdated" : "2021-09-17T20:57:08Z",
                        "Type" : "AWS-HMAC",
                        "AccessKeyId" : "ASIARTEST0",
                        "SecretAccessKey" : "xjtest0",
                        "Token" : "IQote///test0",
                        "Expiration" : "$expiration0"
                    }
                """,
                ),
            )

            // verify that profile is re-retrieved after credentials expiration
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials", "TOKEN_A"),
                imdsResponse("imds-test-role-2"),
            )
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role-2", "TOKEN_A"),
                imdsResponse(
                    """
                    {
                        "Code" : "Success",
                        "LastUpdated" : "2021-09-17T20:57:08Z",
                        "Type" : "AWS-HMAC",
                        "AccessKeyId" : "ASIARTEST1",
                        "SecretAccessKey" : "xjtest1",
                        "Token" : "IQote///test1",
                        "Expiration" : "$expiration1"
                    }
                """,
                ),
            )
        }

        val client = ImdsClient {
            engine = connection
            clock = testClock
        }

        val provider = ImdsCredentialsProvider(
            client = lazyOf(client),
            clock = testClock,
            platformProvider = ec2MetadataEnabledPlatform,
        )

        val actual0 = provider.resolve()
        val expected0 = Credentials(
            "ASIARTEST0",
            "xjtest0",
            "IQote///test0",
            expiration0,
            "IMDSv2",
        )
        assertEquals(expected0, actual0)

        testClock.advance(1.seconds)

        val actual1 = provider.resolve()
        val expected1 = Credentials(
            "ASIARTEST1",
            "xjtest1",
            "IQote///test1",
            expiration1,
            "IMDSv2",
        )
        assertEquals(expected1, actual1)
    }

    @Test
    fun testSuccessProfileOverride() = runTest {
        val testClock = ManualClock()
        val expiration = Instant.fromEpochMilliseconds(testClock.now().epochMilliseconds)

        val connection = buildTestConnection {
            expect(
                tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS),
                tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"),
            )
            // no request for profile, go directly to retrieving role credentials
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"),
                imdsResponse(
                    """
                    {
                        "Code" : "Success",
                        "LastUpdated" : "2021-09-17T20:57:08Z",
                        "Type" : "AWS-HMAC",
                        "AccessKeyId" : "ASIARTEST",
                        "SecretAccessKey" : "xjtest",
                        "Token" : "IQote///test",
                        "Expiration" : "$expiration"
                    }
                """,
                ),
            )
        }

        val client = ImdsClient {
            engine = connection
            clock = testClock
        }

        val provider = ImdsCredentialsProvider(
            profileOverride = "imds-test-role",
            client = lazyOf(client),
            clock = testClock,
            platformProvider = ec2MetadataEnabledPlatform,
        )

        val actual = provider.resolve()
        val expected = Credentials(
            "ASIARTEST",
            "xjtest",
            "IQote///test",
            expiration,
            "IMDSv2",
        )
        assertEquals(expected, actual)
    }

    @Test
    fun testTokenFailure() = runTest {
        // when attempting to retrieve initial token, IMDS replied with 403, indicating IMDS is disabled or not allowed through permissions
        val connection = buildTestConnection {
            expect(
                tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS),
                HttpResponse(HttpStatusCode.Forbidden, Headers.Empty, HttpBody.Empty),
            )
        }

        val testClock = ManualClock()
        val client = ImdsClient {
            engine = connection
            clock = testClock
        }

        val provider = ImdsCredentialsProvider(
            client = lazyOf(client),
            clock = testClock,
            platformProvider = ec2MetadataEnabledPlatform,
        )

        val ex = assertFailsWith {
            provider.resolve()
        }
        ex.message.shouldContain("failed to load instance profile")
        assertIs(ex.cause)
        ex.cause!!.message.shouldContain("Request forbidden")
    }

    @Test
    fun testNoInstanceProfileConfigured() = runTest {
        val connection = buildTestConnection {
            expect(
                tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS),
                tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"),
            )
            expect(
                imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials", "TOKEN_A"),
                HttpResponse(
                    HttpStatusCode.NotFound,
                    Headers.Empty,
                    ByteArrayContent(
                        """
                        
                        
                         
                          404 - Not Found
                         
                         
                          

404 - Not Found

""".trimIndent().encodeToByteArray(), ), ), ) } val testClock = ManualClock() val client = ImdsClient { engine = connection clock = testClock } val provider = ImdsCredentialsProvider( client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) assertFailsWith { provider.resolve() }.message.shouldContain("failed to load instance profile") } // SDK can send a request if expired credentials are available. // If the credentials provider can return expired credentials, that means the SDK can use them, // because no other checks are done before using the credentials. @Test fun testCanReturnExpiredCredentials() = runTest { val connection = buildTestConnection { expect( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), ) expect( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" } """, ), ) } val testClock = ManualClock() val client = ImdsClient { engine = connection clock = testClock } val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) val actual = provider.resolve() val expected = Credentials( accessKeyId = "ASIARTEST", secretAccessKey = "xjtest", sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", ) assertEquals(expected, actual) } // SDK can perform 3 successive requests with expired credentials. IMDS must only be called once. @Test fun testSuccessiveRequestsOnlyCallIMDSOnce() = runTest { val connection = buildTestConnection { expect( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), ) expect( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" } """, ), ) } val testClock = ManualClock() val client = spyk( ImdsClient { engine = connection clock = testClock }, ) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) // call resolve 3 times repeat(3) { provider.resolve() } // make sure ImdsClient only gets called once coVerify(exactly = 1) { client.get(any()) } } @Test fun testDontRefreshUntilNextRefreshTimeHasPassed() = runTest { val connection = buildTestConnection { expect( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), ) expect( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" } """, ), ) expect( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "NEWCREDENTIALS", "SecretAccessKey" : "shhh", "Token" : "IQote///test", "Expiration" : "2022-10-05T03:31:56Z" } """, ), ) } val testClock = ManualClock() val client = spyk( ImdsClient { engine = connection clock = testClock }, ) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) val first = provider.resolve() testClock.advance(20.minutes) // 20 minutes later, we should try to refresh the expired credentials val second = provider.resolve() coVerify(exactly = 2) { client.get(any()) } // make sure we did not just serve the previous credentials assertNotEquals(first, second) } @Test fun testUsesPreviousCredentialsOnReadTimeout() = runTest { val testClock = ManualClock() // this engine throws read timeout exceptions for any requests after the initial one // (i.e allow 1 TTL token and 1 credentials request) val readTimeoutEngine = object : HttpClientEngineBase("readTimeout") { var successfulCallCount = 0 override val config: HttpClientEngineConfig = HttpClientEngineConfig.Default override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { if (successfulCallCount >= 2) { throw SdkIOException() } else { successfulCallCount += 1 return when (successfulCallCount) { 1 -> HttpCall( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), testClock.now(), testClock.now(), ) else -> HttpCall( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }""", ), testClock.now(), testClock.now(), ) } } } } val client = ImdsClient { engine = readTimeoutEngine clock = testClock } val previousCredentials = Credentials( accessKeyId = "ASIARTEST", secretAccessKey = "xjtest", sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", ) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) // call the engine the first time to get a proper credentials response from IMDS val credentials = provider.resolve() assertEquals(credentials, previousCredentials) // call it again and get a read timeout exception from the engine val newCredentials = provider.resolve() // should cause provider to return the previously-served credentials assertEquals(newCredentials, previousCredentials) } @Test fun testThrowsExceptionOnReadTimeoutWhenMissingPreviousCredentials() = runTest { val readTimeoutEngine = TestEngine { _, _ -> throw SdkIOException() } val testClock = ManualClock() val client = ImdsClient { engine = readTimeoutEngine clock = testClock } val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) // a read timeout should cause an exception to be thrown, because we have no previous credentials to re-serve assertFailsWith { provider.resolve() } } @Test fun testUsesPreviousCredentialsOnServerError() = runTest { val testClock = ManualClock() // this engine returns 500 errors for any requests after the initial one (i.e allow 1 TTL token and 1 credentials request) val internalServerErrorEngine = object : HttpClientEngineBase("internalServerError") { var successfulCallCount = 0 override val config: HttpClientEngineConfig = HttpClientEngineConfig.Default override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { if (successfulCallCount >= 2) { return HttpCall( HttpRequest(HttpMethod.GET, Url(Scheme.HTTP, Host.parse("test"), Scheme.HTTP.defaultPort, "/path/foo/bar"), Headers.Empty, HttpBody.Empty), HttpResponse(HttpStatusCode.InternalServerError, Headers.Empty, HttpBody.Empty), testClock.now(), testClock.now(), ) } else { successfulCallCount += 1 return when (successfulCallCount) { 1 -> HttpCall( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), testClock.now(), testClock.now(), ) else -> HttpCall( imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), imdsResponse( """ { "Code" : "Success", "LastUpdated" : "2021-09-17T20:57:08Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", "Expiration" : "2021-09-18T03:31:56Z" }""", ), testClock.now(), testClock.now(), ) } } } } val client = ImdsClient { engine = internalServerErrorEngine clock = testClock } val previousCredentials = Credentials( accessKeyId = "ASIARTEST", secretAccessKey = "xjtest", sessionToken = "IQote///test", expiration = Instant.fromEpochSeconds(1631935916), providerName = "IMDSv2", ) val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) // call the engine the first time to get a proper credentials response from IMDS val credentials = provider.resolve() assertEquals(previousCredentials, credentials) // call it again and get a 500 error from the engine val newCredentials = provider.resolve() // should cause provider to return the previously-served credentials assertEquals(newCredentials, previousCredentials) } @Test fun testThrowsExceptionOnServerErrorWhenMissingPreviousCredentials() = runTest { val testClock = ManualClock() // this engine just returns 500 errors val internalServerErrorEngine = TestEngine { _, _ -> HttpCall( HttpRequest(HttpMethod.GET, Url(Scheme.HTTP, Host.parse("test"), Scheme.HTTP.defaultPort, "/path/foo/bar"), Headers.Empty, HttpBody.Empty), HttpResponse(HttpStatusCode.InternalServerError, Headers.Empty, HttpBody.Empty), testClock.now(), testClock.now(), ) } val client = ImdsClient { engine = internalServerErrorEngine clock = testClock } val provider = ImdsCredentialsProvider( profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock, platformProvider = ec2MetadataEnabledPlatform, ) assertFailsWith { provider.resolve() } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy