-
Notifications
You must be signed in to change notification settings - Fork 55
refactor(rt): generated sts and sts web identity credential providers #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
||
| override suspend fun getCredentials(): Credentials { | ||
| val logger = Logger.getLogger<StsAssumeRoleCredentialsProvider>() | ||
| logger.debug { "retrieving assumed credentials" } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`" | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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