Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions aws-runtime/aws-config/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ kotlin {
implementation("aws.sdk.kotlin.crt:aws-crt-kotlin:$crtKotlinVersion")
implementation(project(":aws-runtime:crt-util"))

// additional dependencies required by generated sts provider
implementation("aws.smithy.kotlin:serde-form-url:$smithyKotlinVersion")
implementation("aws.smithy.kotlin:serde-xml:$smithyKotlinVersion")
implementation(project(":aws-runtime:protocols:aws-xml-protocols"))
implementation(project(":aws-runtime:aws-endpoint"))
implementation(project(":aws-runtime:aws-signing"))

// additional dependencies required by generated sso provider
implementation(project(":aws-runtime:protocols:aws-json-protocols"))
}
Expand Down Expand Up @@ -70,6 +77,41 @@ codegen {
val basePackage = "aws.sdk.kotlin.runtime.auth.credentials.internal"

projections {

// generate an sts client
create("sts-credentials-provider") {
imports = listOf(
awsModelFile("sts.2011-06-15.json")
)

smithyKotlinPlugin {
serviceShapeId = "com.amazonaws.sts#AWSSecurityTokenServiceV20110615"
packageName = "${basePackage}.sts"
packageVersion = project.version.toString()
packageDescription = "Internal STS credentials provider"
sdkId = "STS"
buildSettings {
generateDefaultBuildFiles = false
generateFullProject = false
}
}

// TODO - could we add a trait such that we change visibility to `internal` or a build setting...?
transforms = listOf(
"""
{
"name": "awsSdkKotlinIncludeOperations",
"args": {
"operations": [
"com.amazonaws.sts#AssumeRole",
"com.amazonaws.sts#AssumeRoleWithWebIdentity"
]
}
}
"""
)
}

// generate an sso client
create("sso-credentials-provider") {
imports = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import kotlin.time.Duration
import kotlin.time.ExperimentalTime

private const val DEFAULT_CREDENTIALS_REFRESH_BUFFER_SECONDS = 10
private const val DEFAULT_CREDENTIALS_REFRESH_SECONDS = 60 * 15
/**
* The amount of time credentials are valid for before being refreshed when an explicit value
* is not given to/from a provider
*/
public const val DEFAULT_CREDENTIALS_REFRESH_SECONDS: Int = 60 * 15

/**
* Creates a provider that functions as a caching decorator of another provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,97 @@

package aws.sdk.kotlin.runtime.auth.credentials

import aws.sdk.kotlin.crt.auth.credentials.build
import aws.sdk.kotlin.runtime.crt.SdkDefaultIO
import aws.sdk.kotlin.crt.auth.credentials.StsAssumeRoleCredentialsProvider as StsAssumeRoleCredentialsProviderCrt
import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.StsClient
import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.model.RegionDisabledException
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.resolve
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
import aws.smithy.kotlin.runtime.logging.Logger
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.TimestampFormat
import aws.smithy.kotlin.runtime.time.epochMilliseconds
import aws.smithy.kotlin.runtime.util.Platform
import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

private const val GLOBAL_STS_PARTITION_ENDPOINT = "aws-global"
private const val PROVIDER_NAME = "AssumeRoleProvider"

/**
* A provider that gets credentials from the STS assume role credential provider.
* A [CredentialsProvider] that uses another provider to assume a role from the AWS Security Token Service (STS).
*
* When asked to provide credentials, this provider will first invoke the inner credentials provider
* to get AWS credentials for STS. Then, it will call STS to get assumed credentials for the desired role.
*
* @param credentialsProvider The underlying Credentials Provider to use for source credentials
* @param roleArn The target role's ARN
* @param sessionName The name to associate with the session
* @param durationSeconds The number of seconds from authentication that the session is valid for
* @param credentialsProvider The underlying provider to use for source credentials
* @param roleArn The ARN of the target role to assume, e.g. `arn:aws:iam:123456789:role/example`
* @param region The AWS region to assume the role in. If not set then the global STS endpoint will be used.
* @param roleSessionName The name to associate with the session. Use the role session name to uniquely identify a session
* when the same role is assumed by different principals or for different reasons. In cross-account scenarios, the
* role session name is visible to, and can be logged by the account that owns the role. The role session name is also
* in the ARN of the assumed role principal.
* @param externalId A unique identifier that might be required when you assume a role in another account. If the
* administrator of the account to which the role belongs provided you with an external ID, then provide that value
* in this parameter.
* @param duration The expiry duration of the STS credentials. Defaults to 15 minutes if not set.
* @param httpClientEngine The [HttpClientEngine] to use when making requests to the STS service
*/
public class StsAssumeRoleCredentialsProvider public constructor(
credentialsProvider: CredentialsProvider,
roleArn: String,
sessionName: String,
durationSeconds: Int? = null,
) : CrtCredentialsProvider {
override val crtProvider: StsAssumeRoleCredentialsProviderCrt = StsAssumeRoleCredentialsProviderCrt.build {
clientBootstrap = SdkDefaultIO.ClientBootstrap
tlsContext = SdkDefaultIO.TlsContext
this.credentialsProvider = asCrt(credentialsProvider)
this.roleArn = roleArn
this.sessionName = sessionName
this.durationSeconds = durationSeconds
@OptIn(ExperimentalTime::class)
public class StsAssumeRoleCredentialsProvider(
private val credentialsProvider: CredentialsProvider,
private val roleArn: String,
private val region: String? = null,
private val roleSessionName: String? = null,
private val externalId: String? = null,
private val duration: Duration = Duration.seconds(DEFAULT_CREDENTIALS_REFRESH_SECONDS),
private val httpClientEngine: HttpClientEngine? = null
) : CredentialsProvider {
Comment on lines -20 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is a breaking change so the commit message should have a ! after the type/scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can/will be fixed up in the squashed commit


override suspend fun getCredentials(): Credentials {
val logger = Logger.getLogger<StsAssumeRoleCredentialsProvider>()
logger.debug { "retrieving assumed credentials" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: This log message could be made more informative by including additional details like the role ARN, session name, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is role ARN potentially considered sensitive?


// NOTE: multi region access points require regional STS endpoints
val provider = this
val client = StsClient {
region = provider.region ?: GLOBAL_STS_PARTITION_ENDPOINT
credentialsProvider = provider.credentialsProvider
httpClientEngine = provider.httpClientEngine
}

val resp = try {
client.assumeRole {
roleArn = provider.roleArn
externalId = provider.externalId
roleSessionName = provider.roleSessionName ?: defaultSessionName()
durationSeconds = provider.duration.inWholeSeconds.toInt()
}
} catch (ex: Exception) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concern

This catch seems overly broad. It seems that we'd want to pass I/O exceptions (any anything else that is volatile/environmental) back up directly rather than wrapping in another exception type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after team discussion it seems that this is the preferred treatment for io exceptions

logger.debug { "sts refused to grant assumed role credentials" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Related to previous comment, in the case of say an IO exception this debug message may be misleading to readers, implying some policy constraint in STS where in fact it was something more basic.

when (ex) {
is RegionDisabledException -> throw ProviderConfigurationException(
"STS is not activated in the requested region (${client.config.region}). Please check your configuration and activate STS in the target region if necessary",
ex
)
else -> throw CredentialsProviderException("failed to assume role from STS", ex)
}
} finally {
client.close()
}

val roleCredentials = resp.credentials ?: throw CredentialsProviderException("STS credentials must not be null")
logger.debug { "obtained assumed credentials; expiration=${roleCredentials.expiration?.format(TimestampFormat.ISO_8601)}" }

return Credentials(
accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in STS assumeRole response" },
secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in STS assumeRole response" },
sessionToken = roleCredentials.sessionToken,
expiration = roleCredentials.expiration
)
}
}

// role session name must be provided to assume a role, when the user doesn't provide one we choose a name for them
internal fun defaultSessionName(platformEnvironProvider: PlatformEnvironProvider = Platform): String =
AwsSdkSetting.AwsRoleSessionName.resolve(platformEnvironProvider) ?: "aws-sdk-kotlin-${Instant.now().epochMilliseconds}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question

I assume the prefix aws-sdk-kotlin is meant to disambiguate from something else? where would this string be visible to to the customer, if anywhere? (I'm trying to understand the value of the static string prefix)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It ends up in the assumed role arn, e.g. arn:aws:sts::123456789012:<role-name>/<session-name>

You can see examples in Go and Rust. I opted to follow Go here as I think it's more clear to us and a customer that it is an inferred value.

Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,111 @@

package aws.sdk.kotlin.runtime.auth.credentials

import aws.sdk.kotlin.crt.auth.credentials.build
import aws.sdk.kotlin.runtime.crt.SdkDefaultIO
import aws.sdk.kotlin.crt.auth.credentials.StsWebIdentityCredentialsProvider as StsWebIdentityCredentialsProviderCrt
import aws.sdk.kotlin.runtime.auth.credentials.internal.sts.StsClient
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.resolve
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
import aws.smithy.kotlin.runtime.logging.Logger
import aws.smithy.kotlin.runtime.time.TimestampFormat
import aws.smithy.kotlin.runtime.util.Platform
import aws.smithy.kotlin.runtime.util.PlatformProvider
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

private const val PROVIDER_NAME = "WebIdentityToken"

/**
* A provider that gets credentials from the STS web identity credential provider.
* A [CredentialsProvider] that exchanges a Web Identity Token for credentials from the AWS Security Token Service (STS).
*
* @param roleArn The ARN of the target role to assume, e.g. `arn:aws:iam:123456789:role/example`
* @param webIdentityTokenFilePath The path to the file containing a JWT token
* @param region The AWS region to assume the role in
* @param roleSessionName The name to associate with the session. Use the role session name to uniquely identify a session
* when the same role is assumed by different principals or for different reasons. In cross-account scenarios, the
* role session name is visible to, and can be logged by the account that owns the role. The role session name is also
* in the ARN of the assumed role principal.
* @param duration The expiry duration of the credentials. Defaults to 15 minutes if not set.
* @param platformProvider The platform API provider
* @param httpClientEngine The [HttpClientEngine] to use when making requests to the STS service
*/
public class StsWebIdentityCredentialsProvider : CrtCredentialsProvider {
override val crtProvider: StsWebIdentityCredentialsProviderCrt = StsWebIdentityCredentialsProviderCrt.build {
clientBootstrap = SdkDefaultIO.ClientBootstrap
tlsContext = SdkDefaultIO.TlsContext
@OptIn(ExperimentalTime::class)
public class StsWebIdentityCredentialsProvider(
private val roleArn: String,
private val webIdentityTokenFilePath: String,
private val region: String,
private val roleSessionName: String? = null,
private val duration: Duration = Duration.seconds(DEFAULT_CREDENTIALS_REFRESH_SECONDS),
private val platformProvider: PlatformProvider = Platform,
private val httpClientEngine: HttpClientEngine? = null
) : CredentialsProvider {

public companion object {
/**
* Create an [StsWebIdentityCredentialsProvider] from the current execution environment. This will attempt
* to automatically resolve any setting not explicitly provided from the current set of environment variables
* or system properties.
*/
public fun fromEnvironment(
roleArn: String? = null,
webIdentityTokenFilePath: String? = null,
region: String? = null,
roleSessionName: String? = null,
duration: Duration = Duration.seconds(DEFAULT_CREDENTIALS_REFRESH_SECONDS),
platformProvider: PlatformProvider = Platform,
httpClientEngine: HttpClientEngine? = null
): StsWebIdentityCredentialsProvider {
val resolvedRoleArn = platformProvider.resolve(roleArn, AwsSdkSetting.AwsRoleArn, "roleArn")
val resolvedTokenFilePath = platformProvider.resolve(webIdentityTokenFilePath, AwsSdkSetting.AwsWebIdentityTokenFile, "webIdentityTokenFilePath")
val resolvedRegion = platformProvider.resolve(region, AwsSdkSetting.AwsRegion, "region")
return StsWebIdentityCredentialsProvider(resolvedRoleArn, resolvedTokenFilePath, resolvedRegion, roleSessionName, duration, platformProvider, httpClientEngine)
}
}

override suspend fun getCredentials(): Credentials {
val logger = Logger.getLogger<StsAssumeRoleCredentialsProvider>()
logger.debug { "retrieving assumed credentials via web identity" }
val provider = this

val token = platformProvider
.readFileOrNull(webIdentityTokenFilePath)
?.decodeToString() ?: throw CredentialsProviderException("failed to read webIdentityToken from $webIdentityTokenFilePath")

val client = StsClient {
region = provider.region
httpClientEngine = provider.httpClientEngine
// NOTE: credentials provider not needed for this operation
}

val resp = try {
client.assumeRoleWithWebIdentity {
roleArn = provider.roleArn
webIdentityToken = token
durationSeconds = provider.duration.inWholeSeconds.toInt()
roleSessionName = provider.roleSessionName ?: defaultSessionName(platformProvider)
}
} catch (ex: Exception) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concern

same as above regarding capturing I/O exception types

logger.debug { "sts refused to grant assumed role credentials from web identity" }
throw CredentialsProviderException("STS failed to assume role from web identity", ex)
} finally {
client.close()
}

val roleCredentials = resp.credentials ?: throw CredentialsProviderException("STS credentials must not be null")
logger.debug { "obtained assumed credentials via web identity; expiration=${roleCredentials.expiration?.format(TimestampFormat.ISO_8601)}" }

return Credentials(
accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in STS assumeRoleWithWebIdentity response" },
secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in STS assumeRoleWithWebIdentity response" },
sessionToken = roleCredentials.sessionToken,
expiration = roleCredentials.expiration
)
}
}

// convenience function to resolve parameters for fromEnvironment()
private inline fun <reified T> PlatformProvider.resolve(explicit: T?, setting: AwsSdkSetting<T>, name: String): T {
return explicit ?: setting.resolve(this)
?: throw ProviderConfigurationException(
"Required field `$name` could not be automatically inferred for StsWebIdentityCredentialsProvider. Either explicitly pass a value, set the environment variable `${setting.environmentVariable}`, or set the JVM system property `${setting.jvmProperty}`"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ public sealed class AwsSdkSetting<T>(
* The endpoint mode to use when connecting to the EC2 metadata service endpoint
*/
public object AwsEc2MetadataServiceEndpointMode : AwsSdkSetting<String>("AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE", "aws.ec2MetadataServiceEndpointMode")

// TODO - Currently env/system properties around role ARN, role session name, etc are restricted to the STS web identity provider.
// They should be applied more broadly but this needs fleshed out across AWS SDKs before we can do so.

/**
* The ARN of a role to assume
*/
public object AwsRoleArn : AwsSdkSetting<String>("AWS_ROLE_ARN", "aws.roleArn")

/**
* The session name to use for assumed roles
*/
public object AwsRoleSessionName : AwsSdkSetting<String>("AWS_ROLE_SESSION_NAME", "aws.roleSessionName")

/**
* The AWS web identity token file path
*/
public object AwsWebIdentityTokenFile : AwsSdkSetting<String>("AWS_WEB_IDENTITY_TOKEN_FILE", "aws.webIdentityTokenFile")
}

/**
Expand Down
Loading