Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changes/afeacad1-0f2c-483d-a755-4df1fd6fd440.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "afeacad1-0f2c-483d-a755-4df1fd6fd440",
"type": "feature",
"description": "Validate caller-specified AWS regions in client config (i.e., `region` and `regionProvider`)"
}
5 changes: 5 additions & 0 deletions aws-runtime/aws-config/api/aws-config.api
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,8 @@ public final class aws/sdk/kotlin/runtime/region/ResolveRegionKt {
public static synthetic fun resolveSigV4aSigningRegionSet$default (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class aws/sdk/kotlin/runtime/region/ValidateRegionKt {
public static final fun isRegionValid (Ljava/lang/String;)Z
public static final fun validateRegion (Ljava/lang/String;)Ljava/lang/String;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.region

import aws.sdk.kotlin.runtime.ConfigurationException
import aws.sdk.kotlin.runtime.InternalSdkApi

internal fun charSet(chars: String) = chars.toCharArray().toSet()
internal fun charSet(range: CharRange) = range.toSet()

private object Rfc3986CharSets {
val alpha = charSet('A'..'Z') + charSet('a'..'z')
val digit = charSet('0'..'9')
val unreserved = alpha + digit + charSet("-._~")
val hexdig = digit + charSet('A'..'F')
val pctEncoded = hexdig + '%'
val subDelims = charSet("!$&'()*+,;=")
val regName = unreserved + pctEncoded + subDelims
}

@InternalSdkApi
public fun isRegionValid(region: String): Boolean = region.isNotEmpty() && region.all(Rfc3986CharSets.regName::contains)

@InternalSdkApi
public fun validateRegion(region: String): String = region.also {
if (!isRegionValid(region)) {
throw ConfigurationException("""Configured region "$region" is invalid. A region must be a valid URI host component.""")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.region

import aws.sdk.kotlin.runtime.ConfigurationException
import kotlin.test.*

/**
* Forms the [combinations](https://en.wikipedia.org/wiki/Combination) of a given length for the given set
*/
private fun combinations(ofSet: Set<Char>, length: Int): Set<String> {
if (length <= 0) return emptySet()
if (length == 1) return ofSet.map { it.toString() }.toSet()

val elements = ofSet.toList()

return buildSet {
fun generate(current: String, startIndex: Int) {
if (current.length == length) {
add(current)
} else {
for (i in startIndex until elements.size) {
generate(current + elements[i], i + 1)
}
}
}

generate("", 0)
}
}

private object TestData {
private val validChars = charSet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%!$&'()*+,;=")

/**
* Non-exhaustive set of [actual AWS regions][1].
*
* [1]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
*/
private val realRegions = setOf(
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
)

private val kitchenSinkRegion = validChars.joinToString("") // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.+~%!$&'()*+,;=
private val regionsWithSpecialChars = combinations(validChars, 3).map { "region-$it" }.toSet() // region-XXX
val validRegions = realRegions + regionsWithSpecialChars + kitchenSinkRegion

private val printableAsciiChars = charSet(32.toChar()..126.toChar()) // ASCII codepoints 32-126 (inclusive)
private val invalidChars = printableAsciiChars - validChars
val invalidRegions = combinations(invalidChars, 3).map { "region-$it" }.toSet() // region-XXX
}

class ValidateRegionTest {
@Test
fun testIsRegionValid() {
TestData.validRegions.forEach {
println("Valid region: $it")
assertTrue(isRegionValid(it))
}
TestData.invalidRegions.forEach {
println("Invalid region: $it")
assertFalse(isRegionValid(it))
}
}

@Test
fun testValidateRegion() {
TestData.validRegions.forEach {
assertEquals(it, validateRegion(it))
}

TestData.invalidRegions.forEach {
assertFailsWith<ConfigurationException> {
validateRegion(it)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ object AwsRuntimeTypes {
object Region : RuntimeTypePackage(AwsKotlinDependency.AWS_CONFIG, "region") {
val DefaultRegionProviderChain = symbol("DefaultRegionProviderChain")
val resolveRegion = symbol("resolveRegion")
val validateRegion = symbol("validateRegion")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
*/
package aws.sdk.kotlin.codegen

import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.getContextValue
import software.amazon.smithy.kotlin.codegen.integration.AppendingSectionWriter
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
import software.amazon.smithy.kotlin.codegen.model.asNullable
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.model.nullable
import software.amazon.smithy.kotlin.codegen.rendering.*
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolClientGenerator
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
Expand Down Expand Up @@ -42,11 +44,12 @@ class AwsServiceConfigIntegration : KotlinIntegration {
propertyType = ConfigPropertyType.Custom(
render = { prop, writer ->
writer.write(
"override val #1L: #2T? = builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() }",
"override val #1L: #2T? = (builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() })?.let { #5T(it) }",
prop.propertyName,
prop.symbol,
RuntimeTypes.KotlinxCoroutines.runBlocking,
AwsRuntimeTypes.Config.Region.resolveRegion,
AwsRuntimeTypes.Config.Region.validateRegion,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import org.junit.jupiter.api.Test
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientConfigGenerator
import software.amazon.smithy.kotlin.codegen.test.*
import software.amazon.smithy.kotlin.codegen.test.newTestContext
import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff
import software.amazon.smithy.kotlin.codegen.test.toRenderingContext
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
import software.amazon.smithy.model.shapes.ServiceShape

class AwsServiceConfigIntegrationTest {
Expand Down Expand Up @@ -45,7 +48,7 @@ class AwsServiceConfigIntegrationTest {
val contents = writer.toString()

val expectedProps = """
override val region: String? = builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() }
override val region: String? = (builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() })?.let { validateRegion(it) }
override val regionProvider: RegionProvider = builder.regionProvider ?: DefaultRegionProviderChain()
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider(httpClient = httpClient, region = region).manage()
"""
Expand Down
Loading