Skip to content

Commit 0e44c8c

Browse files
committed
feat: validate caller-provided regions in client config
1 parent 042cb2d commit 0e44c8c

File tree

7 files changed

+158
-5
lines changed

7 files changed

+158
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "afeacad1-0f2c-483d-a755-4df1fd6fd440",
3+
"type": "feature",
4+
"description": "Validate caller-specified AWS regions in client config (i.e., `region` and `regionProvider`)"
5+
}

aws-runtime/aws-config/api/aws-config.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,8 @@ public final class aws/sdk/kotlin/runtime/region/ResolveRegionKt {
707707
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;
708708
}
709709

710+
public final class aws/sdk/kotlin/runtime/region/ValidateRegionKt {
711+
public static final fun isRegionValid (Ljava/lang/String;)Z
712+
public static final fun validateRegion (Ljava/lang/String;)Ljava/lang/String;
713+
}
714+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package aws.sdk.kotlin.runtime.region
2+
3+
import aws.sdk.kotlin.runtime.ConfigurationException
4+
import aws.sdk.kotlin.runtime.InternalSdkApi
5+
6+
internal fun charSet(chars: String) = chars.toCharArray().toSet()
7+
internal fun charSet(range: CharRange) = range.toSet()
8+
9+
private object Rfc3986CharSets {
10+
val alpha = charSet('A'..'Z') + charSet('a'..'z')
11+
val digit = charSet('0'..'9')
12+
val unreserved = alpha + digit + charSet("-.+~")
13+
val hexdig = digit + charSet('A'..'F')
14+
val pctEncoded = hexdig + '%'
15+
val subDelims = charSet("!$&'()*+,;=")
16+
val regName = unreserved + pctEncoded + subDelims
17+
}
18+
19+
@InternalSdkApi
20+
public fun isRegionValid(region: String): Boolean = region.isNotEmpty() && region.all(Rfc3986CharSets.regName::contains)
21+
22+
@InternalSdkApi
23+
public fun validateRegion(region: String): String = region.also {
24+
if (!isRegionValid(region)) {
25+
throw ConfigurationException("""Configured region "$region" is invalid. A region must be a valid URI host component.""")
26+
}
27+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package aws.sdk.kotlin.runtime.region
2+
3+
import aws.sdk.kotlin.runtime.ConfigurationException
4+
import kotlin.test.*
5+
6+
/**
7+
* Forms the [combinations](https://en.wikipedia.org/wiki/Combination) of a given length for the given set
8+
*/
9+
private fun combinations(ofSet: Set<Char>, length: Int): Set<String> {
10+
if (length <= 0) return emptySet()
11+
if (length == 1) return ofSet.map { it.toString() }.toSet()
12+
13+
val elements = ofSet.toList()
14+
15+
return buildSet {
16+
fun generate(current: String, startIndex: Int) {
17+
if (current.length == length) {
18+
add(current)
19+
} else {
20+
for (i in startIndex until elements.size) {
21+
generate(current + elements[i], i + 1)
22+
}
23+
}
24+
}
25+
26+
generate("", 0)
27+
}
28+
}
29+
30+
private object TestData {
31+
private val validChars = charSet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.+~%!$&'()*+,;=")
32+
33+
/**
34+
* Non-exhaustive set of [actual AWS regions][1].
35+
*
36+
* [1]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
37+
*/
38+
private val realRegions = setOf(
39+
"af-south-1",
40+
"ap-east-1",
41+
"ap-east-2",
42+
"ap-northeast-1",
43+
"ap-northeast-2",
44+
"ap-northeast-3",
45+
"ap-south-1",
46+
"ap-south-2",
47+
"ap-southeast-1",
48+
"ap-southeast-2",
49+
"ap-southeast-3",
50+
"ap-southeast-4",
51+
"ap-southeast-5",
52+
"ap-southeast-6",
53+
"ap-southeast-7",
54+
"ca-central-1",
55+
"ca-west-1",
56+
"eu-central-1",
57+
"eu-central-2",
58+
"eu-north-1",
59+
"eu-south-1",
60+
"eu-south-2",
61+
"eu-west-1",
62+
"eu-west-2",
63+
"eu-west-3",
64+
"il-central-1",
65+
"me-central-1",
66+
"me-south-1",
67+
"mx-central-1",
68+
"sa-east-1",
69+
"us-east-1",
70+
"us-east-2",
71+
"us-west-1",
72+
"us-west-2",
73+
)
74+
75+
private val kitchenSinkRegion = validChars.joinToString("") // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.+~%!$&'()*+,;=
76+
private val regionsWithSpecialChars = combinations(validChars, 3).map { "region-$it" }.toSet() // region-XXX
77+
val validRegions = realRegions + regionsWithSpecialChars + kitchenSinkRegion
78+
79+
private val printableAsciiChars = charSet(32.toChar()..126.toChar()) // ASCII codepoints 32-126 (inclusive)
80+
private val invalidChars = printableAsciiChars - validChars
81+
val invalidRegions = combinations(invalidChars, 3).map { "region-$it" }.toSet() // region-XXX
82+
}
83+
84+
class ValidateRegionTest {
85+
@Test
86+
fun testIsRegionValid() {
87+
TestData.validRegions.forEach {
88+
println("Valid region: $it")
89+
assertTrue(isRegionValid(it))
90+
}
91+
TestData.invalidRegions.forEach {
92+
println("Invalid region: $it")
93+
assertFalse(isRegionValid(it))
94+
}
95+
}
96+
97+
@Test
98+
fun testValidateRegion() {
99+
TestData.validRegions.forEach {
100+
assertEquals(it, validateRegion(it))
101+
}
102+
103+
TestData.invalidRegions.forEach {
104+
assertFailsWith<ConfigurationException> {
105+
validateRegion(it)
106+
}
107+
}
108+
}
109+
}

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ object AwsRuntimeTypes {
6464
object Region : RuntimeTypePackage(AwsKotlinDependency.AWS_CONFIG, "region") {
6565
val DefaultRegionProviderChain = symbol("DefaultRegionProviderChain")
6666
val resolveRegion = symbol("resolveRegion")
67+
val validateRegion = symbol("validateRegion")
6768
}
6869
}
6970

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
*/
55
package aws.sdk.kotlin.codegen
66

7-
import software.amazon.smithy.kotlin.codegen.core.*
7+
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
8+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
9+
import software.amazon.smithy.kotlin.codegen.core.getContextValue
810
import software.amazon.smithy.kotlin.codegen.integration.AppendingSectionWriter
911
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
1012
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
1113
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1214
import software.amazon.smithy.kotlin.codegen.model.asNullable
1315
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
1416
import software.amazon.smithy.kotlin.codegen.model.nullable
15-
import software.amazon.smithy.kotlin.codegen.rendering.*
17+
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientGenerator
1618
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolClientGenerator
1719
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
1820
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
@@ -42,11 +44,12 @@ class AwsServiceConfigIntegration : KotlinIntegration {
4244
propertyType = ConfigPropertyType.Custom(
4345
render = { prop, writer ->
4446
writer.write(
45-
"override val #1L: #2T? = builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() }",
47+
"override val #1L: #2T? = (builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() })?.let { #5T(it) }",
4648
prop.propertyName,
4749
prop.symbol,
4850
RuntimeTypes.KotlinxCoroutines.runBlocking,
4951
AwsRuntimeTypes.Config.Region.resolveRegion,
52+
AwsRuntimeTypes.Config.Region.validateRegion,
5053
)
5154
},
5255
)

codegen/aws-sdk-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegrationTest.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import org.junit.jupiter.api.Test
99
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
1010
import software.amazon.smithy.kotlin.codegen.model.expectShape
1111
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientConfigGenerator
12-
import software.amazon.smithy.kotlin.codegen.test.*
12+
import software.amazon.smithy.kotlin.codegen.test.newTestContext
13+
import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff
14+
import software.amazon.smithy.kotlin.codegen.test.toRenderingContext
15+
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
1316
import software.amazon.smithy.model.shapes.ServiceShape
1417

1518
class AwsServiceConfigIntegrationTest {
@@ -45,7 +48,7 @@ class AwsServiceConfigIntegrationTest {
4548
val contents = writer.toString()
4649

4750
val expectedProps = """
48-
override val region: String? = builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() }
51+
override val region: String? = (builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() })?.let { validateRegion(it) }
4952
override val regionProvider: RegionProvider = builder.regionProvider ?: DefaultRegionProviderChain()
5053
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider(httpClient = httpClient, region = region).manage()
5154
"""

0 commit comments

Comments
 (0)