Skip to content

Commit 580f5ab

Browse files
authored
feat: add auth token generators for RDS and DSQL (#1495)
1 parent 99ddf1e commit 580f5ab

File tree

7 files changed

+250
-5
lines changed

7 files changed

+250
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "a2520ae2-1cba-49b1-b720-10b70e52f9e0",
3+
"type": "feature",
4+
"description": "Add auth token generator for RDS and DSQL"
5+
}

.github/workflows/continuous-integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
./gradlew -Ptest.java.version=${{ matrix.java-version }} jvmTest --stacktrace
4343
- name: Save Test Reports
4444
if: failure()
45-
uses: actions/upload-artifact@v3
45+
uses: actions/upload-artifact@v4
4646
with:
4747
name: test-reports
4848
path: '**/build/reports'
@@ -80,7 +80,7 @@ jobs:
8080
./gradlew testAllProtocols
8181
- name: Save Test Reports
8282
if: failure()
83-
uses: actions/upload-artifact@v3
83+
uses: actions/upload-artifact@v4
8484
with:
8585
name: test-reports
8686
path: '**/build/reports'

gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ ksp-version = "2.1.0-1.0.29" # Keep in sync with kotlin-version
44

55
dokka-version = "1.9.10"
66

7-
aws-kotlin-repo-tools-version = "0.4.17"
7+
aws-kotlin-repo-tools-version = "0.4.18"
88

99
# libs
1010
coroutines-version = "1.9.0"
1111
atomicfu-version = "0.25.0"
1212
binary-compatibility-validator-version = "0.16.3"
1313

1414
# smithy-kotlin codegen and runtime are versioned separately
15-
smithy-kotlin-runtime-version = "1.4.0"
16-
smithy-kotlin-codegen-version = "0.34.0"
15+
smithy-kotlin-runtime-version = "1.4.1"
16+
smithy-kotlin-codegen-version = "0.34.1"
1717

1818
# codegen
1919
smithy-version = "1.53.0"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.dsql
6+
7+
import aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider
8+
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
9+
import aws.smithy.kotlin.runtime.auth.awssigning.AuthTokenGenerator
10+
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigner
11+
import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner
12+
import aws.smithy.kotlin.runtime.net.url.Url
13+
import aws.smithy.kotlin.runtime.time.Clock
14+
import kotlin.time.Duration
15+
import kotlin.time.Duration.Companion.seconds
16+
17+
/**
18+
* Generates an IAM authentication token for use with DSQL databases
19+
* @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token, defaults to [DefaultChainCredentialsProvider]
20+
* @param signer The [AwsSigner] implementation to use when creating the authentication token, defaults to [DefaultAwsSigner]
21+
* @param clock The [Clock] implementation to use
22+
*/
23+
public class DsqlAuthTokenGenerator(
24+
public val credentialsProvider: CredentialsProvider = DefaultChainCredentialsProvider(),
25+
public val signer: AwsSigner = DefaultAwsSigner,
26+
public val clock: Clock = Clock.System,
27+
) {
28+
private val generator = AuthTokenGenerator("dsql", credentialsProvider, signer, clock)
29+
30+
/**
31+
* Generates an auth token for the DbConnect action.
32+
* @param endpoint the endpoint of the database
33+
* @param region the region of the database
34+
* @param expiration how long the auth token should be valid for. Defaults to 900.seconds
35+
*/
36+
public suspend fun generateDbConnectAuthToken(endpoint: Url, region: String, expiration: Duration = 900.seconds): String {
37+
val dbConnectEndpoint = endpoint.toBuilder().apply {
38+
parameters.decodedParameters.put("Action", "DbConnect")
39+
}.build()
40+
41+
return generator.generateAuthToken(dbConnectEndpoint, region, expiration)
42+
}
43+
44+
/**
45+
* Generates an auth token for the DbConnectAdmin action.
46+
* @param endpoint the endpoint of the database
47+
* @param region the region of the database
48+
* @param expiration how long the auth token should be valid for. Defaults to 900.seconds
49+
*/
50+
public suspend fun generateDbConnectAdminAuthToken(endpoint: Url, region: String, expiration: Duration = 900.seconds): String {
51+
val dbConnectAdminEndpoint = endpoint.toBuilder().apply {
52+
parameters.decodedParameters.put("Action", "DbConnectAdmin")
53+
}.build()
54+
55+
return generator.generateAuthToken(dbConnectAdminEndpoint, region, expiration)
56+
}
57+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.dsql
6+
7+
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
8+
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
9+
import aws.smithy.kotlin.runtime.net.Host
10+
import aws.smithy.kotlin.runtime.net.url.Url
11+
import aws.smithy.kotlin.runtime.time.Instant
12+
import aws.smithy.kotlin.runtime.time.ManualClock
13+
import kotlinx.coroutines.test.runTest
14+
import kotlin.test.Test
15+
import kotlin.test.assertContains
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertFalse
18+
import kotlin.time.Duration.Companion.seconds
19+
20+
class DsqlAuthTokenGeneratorTest {
21+
@Test
22+
fun testGenerateDbConnectAuthToken() = runTest {
23+
val clock = ManualClock(Instant.fromEpochSeconds(1724716800))
24+
25+
val credentials = Credentials("akid", "secret")
26+
val credentialsProvider = StaticCredentialsProvider(credentials)
27+
28+
val token = DsqlAuthTokenGenerator(credentialsProvider, clock = clock)
29+
.generateDbConnectAuthToken(
30+
endpoint = Url { host = Host.parse("peccy.dsql.us-east-1.on.aws") },
31+
region = "us-east-1",
32+
expiration = 450.seconds,
33+
)
34+
35+
// Token should have a parameter Action=DbConnect
36+
assertContains(token, "peccy.dsql.us-east-1.on.aws?Action=DbConnect")
37+
assertContains(token, "X-Amz-Credential=akid%2F20240827%2Fus-east-1%2Fdsql%2Faws4_request")
38+
assertContains(token, "X-Amz-Expires=450")
39+
assertContains(token, "X-Amz-SignedHeaders=host")
40+
41+
// Token should not contain a scheme
42+
listOf("http://", "https://").forEach {
43+
assertFalse(token.contains(it))
44+
}
45+
46+
val urlToken = Url.parse("https://$token")
47+
val xAmzDate = urlToken.parameters.decodedParameters.getValue("X-Amz-Date").single()
48+
assertEquals(clock.now(), Instant.fromIso8601(xAmzDate))
49+
}
50+
51+
@Test
52+
fun testGenerateDbConnectAuthAdminToken() = runTest {
53+
val clock = ManualClock(Instant.fromEpochSeconds(1724716800))
54+
55+
val credentials = Credentials("akid", "secret")
56+
val credentialsProvider = StaticCredentialsProvider(credentials)
57+
58+
val token = DsqlAuthTokenGenerator(credentialsProvider, clock = clock)
59+
.generateDbConnectAdminAuthToken(
60+
endpoint = Url { host = Host.parse("peccy.dsql.us-east-1.on.aws") },
61+
region = "us-east-1",
62+
expiration = 450.seconds,
63+
)
64+
65+
// Token should have a parameter Action=DbConnectAdmin
66+
assertContains(token, "peccy.dsql.us-east-1.on.aws?Action=DbConnectAdmin")
67+
assertContains(token, "X-Amz-Credential=akid%2F20240827%2Fus-east-1%2Fdsql%2Faws4_request")
68+
assertContains(token, "X-Amz-Expires=450")
69+
assertContains(token, "X-Amz-SignedHeaders=host")
70+
71+
// Token should not contain a scheme
72+
listOf("http://", "https://").forEach {
73+
assertFalse(token.contains(it))
74+
}
75+
76+
val urlToken = Url.parse("https://$token")
77+
val xAmzDate = urlToken.parameters.decodedParameters.getValue("X-Amz-Date").single()
78+
assertEquals(clock.now(), Instant.fromIso8601(xAmzDate))
79+
}
80+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.rds
6+
7+
import aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider
8+
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
9+
import aws.smithy.kotlin.runtime.auth.awssigning.AuthTokenGenerator
10+
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigner
11+
import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner
12+
import aws.smithy.kotlin.runtime.net.url.Url
13+
import aws.smithy.kotlin.runtime.time.Clock
14+
import kotlin.apply
15+
import kotlin.time.Duration
16+
import kotlin.time.Duration.Companion.seconds
17+
18+
/**
19+
* Generates an IAM authentication token for use with RDS databases
20+
* @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token, defaults to [DefaultChainCredentialsProvider]
21+
* @param signer The [AwsSigner] implementation to use when creating the authentication token, defaults to [DefaultAwsSigner]
22+
* @param clock The [Clock] implementation to use
23+
*/
24+
public class RdsAuthTokenGenerator(
25+
public val credentialsProvider: CredentialsProvider = DefaultChainCredentialsProvider(),
26+
public val signer: AwsSigner = DefaultAwsSigner,
27+
public val clock: Clock = Clock.System,
28+
) {
29+
private val generator = AuthTokenGenerator("rds-db", credentialsProvider, signer, clock)
30+
31+
/**
32+
* Generates an auth token for the `connect` action.
33+
* @param endpoint the endpoint of the database
34+
* @param region the region of the database
35+
* @param username the username to authenticate with
36+
* @param expiration how long the auth token should be valid for. Defaults to 900.seconds
37+
*/
38+
public suspend fun generateAuthToken(endpoint: Url, region: String, username: String, expiration: Duration = 900.seconds): String {
39+
val endpoint = endpoint.toBuilder().apply {
40+
parameters.decodedParameters.apply {
41+
put("Action", "connect")
42+
put("DBUser", username)
43+
}
44+
}.build()
45+
46+
return generator.generateAuthToken(endpoint, region, expiration)
47+
}
48+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.rds
6+
7+
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
8+
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
9+
import aws.smithy.kotlin.runtime.net.Host
10+
import aws.smithy.kotlin.runtime.net.url.Url
11+
import aws.smithy.kotlin.runtime.time.Instant
12+
import aws.smithy.kotlin.runtime.time.ManualClock
13+
import kotlinx.coroutines.test.runTest
14+
import kotlin.test.Test
15+
import kotlin.test.assertContains
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertFalse
18+
import kotlin.time.Duration.Companion.seconds
19+
20+
class RdsAuthTokenGeneratorTest {
21+
@Test
22+
fun testGenerateAuthToken() = runTest {
23+
val clock = ManualClock(Instant.fromEpochSeconds(1724716800))
24+
25+
val credentials = Credentials("akid", "secret")
26+
val credentialsProvider = StaticCredentialsProvider(credentials)
27+
28+
val generator = RdsAuthTokenGenerator(credentialsProvider, clock = clock)
29+
30+
val token = generator.generateAuthToken(
31+
endpoint = Url {
32+
host = Host.parse("prod-instance.us-east-1.rds.amazonaws.com")
33+
port = 3306
34+
},
35+
region = "us-east-1",
36+
username = "peccy",
37+
expiration = 450.seconds,
38+
)
39+
40+
// Token should have a parameter Action=connect, DBUser=peccy
41+
assertContains(token, "prod-instance.us-east-1.rds.amazonaws.com:3306?Action=connect&DBUser=peccy")
42+
assertContains(token, "X-Amz-Credential=akid%2F20240827%2Fus-east-1%2Frds-db%2Faws4_request")
43+
assertContains(token, "X-Amz-Expires=450")
44+
assertContains(token, "X-Amz-SignedHeaders=host")
45+
46+
// Token should not contain a scheme
47+
listOf("http://", "https://").forEach {
48+
assertFalse(token.contains(it))
49+
}
50+
51+
val urlToken = Url.parse("https://$token")
52+
val xAmzDate = urlToken.parameters.decodedParameters.getValue("X-Amz-Date").single()
53+
assertEquals(clock.now(), Instant.fromIso8601(xAmzDate))
54+
}
55+
}

0 commit comments

Comments
 (0)