Skip to content

Commit 5d57469

Browse files
authored
refactor: presigner generates querystring signed requests (#388)
1 parent fafd1e6 commit 5d57469

File tree

9 files changed

+221
-33
lines changed

9 files changed

+221
-33
lines changed

aws-runtime/aws-signing/common/src/aws/sdk/kotlin/runtime/auth/signing/Presigner.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public enum class SigningLocation {
5959
* @property path HTTP path of the presigned request
6060
* @property queryString the HTTP querystring of the presigned request
6161
* @property durationSeconds Number of seconds that the request will be valid for after being signed.
62-
* @property hasBody Specifies if the request contains a body
62+
* @property signBody Specifies if the request body should be signed
6363
* @property signingLocation Specifies where the signing information should be placed in the presigned request
6464
* @property additionalHeaders Custom headers that should be signed as part of the request
6565
*/
@@ -68,7 +68,7 @@ public data class PresignedRequestConfig(
6868
public val path: String,
6969
public val queryString: QueryParameters = QueryParameters.Empty,
7070
public val durationSeconds: Long,
71-
public val hasBody: Boolean = false,
71+
public val signBody: Boolean = false,
7272
public val signingLocation: SigningLocation,
7373
public val additionalHeaders: Headers = Headers.Empty
7474
)
@@ -90,7 +90,7 @@ public suspend fun createPresignedRequest(serviceConfig: ServicePresignConfig, r
9090
credentials = crtCredentials
9191
signatureType = if (requestConfig.signingLocation == SigningLocation.HEADER) AwsSignatureType.HTTP_REQUEST_VIA_HEADERS else AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
9292
signedBodyHeader = AwsSignedBodyHeaderType.X_AMZ_CONTENT_SHA256
93-
signedBodyValue = if (requestConfig.hasBody) AwsSignedBodyValue.UNSIGNED_PAYLOAD else null
93+
signedBodyValue = if (requestConfig.signBody) null else AwsSignedBodyValue.UNSIGNED_PAYLOAD
9494
expirationInSeconds = requestConfig.durationSeconds
9595
}
9696

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/PresignerGenerator.kt

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package aws.sdk.kotlin.codegen
22

33
import aws.sdk.kotlin.codegen.model.traits.Presignable
44
import aws.sdk.kotlin.codegen.protocols.core.AwsEndpointResolverGenerator
5-
import aws.sdk.kotlin.codegen.protocols.core.QueryBindingResolver
65
import aws.sdk.kotlin.codegen.protocols.middleware.AwsSignatureVersion4
76
import software.amazon.smithy.aws.traits.auth.SigV4Trait
87
import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait
@@ -31,8 +30,6 @@ import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigProperty
3130
import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigPropertyType
3231
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpBindingProtocolGenerator
3332
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpBindingResolver
34-
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpTraitResolver
35-
import software.amazon.smithy.kotlin.codegen.rendering.protocol.hasHttpBody
3633
import software.amazon.smithy.kotlin.codegen.rendering.serde.serializerName
3734
import software.amazon.smithy.kotlin.codegen.utils.dq
3835
import software.amazon.smithy.model.shapes.OperationShape
@@ -46,16 +43,13 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait
4643
*
4744
* @property serviceId ID of service presigning applies to
4845
* @property operationId Operation capable of presigning
49-
* @property presignedParameterId (Optional) parameter in which presigned URL should be passed in the request
50-
* @property hasBody true if operation will pass an unsigned body with the request
46+
* @property signBody true if the body is to be read and signed, otherwise body specified as unsigned.
5147
*
5248
*/
5349
data class PresignableOperation(
5450
val serviceId: String,
5551
val operationId: String,
56-
// TODO ~ Implementation of embedded presigned URLs is TBD
57-
val presignedParameterId: String?,
58-
val hasBody: Boolean,
52+
val signBody: Boolean,
5953
)
6054

6155
/**
@@ -103,9 +97,10 @@ class PresignerGenerator : KotlinIntegration {
10397
.filter { operationShape -> operationShape.hasTrait(Presignable.ID) }
10498
.map { operationShape ->
10599
check(AwsSignatureVersion4.hasSigV4AuthScheme(ctx.model, service, operationShape)) { "Operation does not have valid auth trait" }
106-
val resolver: HttpBindingResolver = getProtocolHttpBindingResolver(ctx, service)
107-
val hasBody = resolver.hasHttpBody(operationShape)
108-
PresignableOperation(service.id.toString(), operationShape.id.toString(), null, hasBody)
100+
val protocol = requireNotNull(ctx.protocolGenerator).protocol.name
101+
val shouldSignBody = signBody(protocol)
102+
103+
PresignableOperation(service.id.toString(), operationShape.id.toString(), shouldSignBody)
109104
}
110105

111106
// If presignable operations found for this service, generate a Presigner file
@@ -116,6 +111,16 @@ class PresignerGenerator : KotlinIntegration {
116111
}
117112
}
118113

114+
// Determine if body should be read and signed by CRT. If body is to be signed by CRT, null is passed to signer
115+
// for signedBodyValue parameter. This causes CRT to read the body and compute the signature.
116+
// Otherwise, AwsSignedBodyValue.UNSIGNED_PAYLOAD is passed specifying that the body will be ignored and CRT
117+
// will not take the body into account when signing the request.
118+
private fun signBody(protocol: String) =
119+
when (protocol) {
120+
"awsQuery" -> true // Query protocol always contains a body
121+
else -> false
122+
}
123+
119124
private fun renderPresigner(
120125
writer: KotlinWriter,
121126
ctx: CodegenContext,
@@ -302,7 +307,7 @@ class PresignerGenerator : KotlinIntegration {
302307
write("httpRequestBuilder.url.path,")
303308
presignConfigFnVisitor.renderQueryParameters(writer)
304309
write("durationSeconds.toLong(),")
305-
write("${presignableOp.hasBody},")
310+
write("${presignableOp.signBody},")
306311
write("SigningLocation.HEADER")
307312
}
308313
}
@@ -345,12 +350,4 @@ class PresignerGenerator : KotlinIntegration {
345350
write("return createPresignedRequest(presignConfig, $requestConfigFnName(this, durationSeconds))")
346351
}
347352
}
348-
349-
private fun getProtocolHttpBindingResolver(ctx: CodegenContext, service: ServiceShape): HttpBindingResolver =
350-
when (requireNotNull(ctx.protocolGenerator).protocol) {
351-
AwsQueryTrait.ID -> QueryBindingResolver(ctx.model, service)
352-
RestJson1Trait.ID -> HttpTraitResolver(ctx.model, service, "application/json")
353-
RestXmlTrait.ID -> HttpTraitResolver(ctx.model, service, "application/xml")
354-
else -> throw CodegenException("Unable to create HttpBindingResolver for unhandled protocol ${ctx.protocolGenerator?.protocol}")
355-
}
356353
}

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/polly/PollyPresigner.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class PollyPresigner : KotlinIntegration {
6262
httpRequestBuilder.url.path,
6363
queryStringBuilder.build(),
6464
durationSeconds.toLong(),
65-
false,
65+
true,
6666
SigningLocation.QUERY_STRING
6767
)
6868
""".trimIndent()

codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/PresignerGeneratorTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class PresignerGeneratorTest {
176176
httpRequestBuilder.url.path,
177177
httpRequestBuilder.url.parameters.build(),
178178
durationSeconds.toLong(),
179-
true,
179+
false,
180180
SigningLocation.HEADER
181181
)
182182
}
@@ -214,7 +214,7 @@ class PresignerGeneratorTest {
214214
httpRequestBuilder.url.path,
215215
httpRequestBuilder.url.parameters.build(),
216216
durationSeconds.toLong(),
217-
true,
217+
false,
218218
SigningLocation.HEADER
219219
)
220220
}
@@ -233,7 +233,7 @@ class PresignerGeneratorTest {
233233
companion object {
234234
@JvmStatic
235235
fun fluentBuilder(): FluentBuilder = BuilderImpl()
236-
236+
237237
operator fun invoke(block: DslBuilder.() -> kotlin.Unit): ServicePresignConfig = BuilderImpl().apply(block).build()
238238
}
239239
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package aws.sdk.kotlin.services.polly
2+
3+
import aws.sdk.kotlin.runtime.http.engine.crt.CrtHttpEngine
4+
import aws.sdk.kotlin.services.polly.model.OutputFormat
5+
import aws.sdk.kotlin.services.polly.model.SynthesizeSpeechRequest
6+
import aws.sdk.kotlin.services.polly.model.VoiceId
7+
import aws.smithy.kotlin.runtime.http.response.complete
8+
import aws.smithy.kotlin.runtime.http.sdkHttpClient
9+
import aws.smithy.kotlin.runtime.testing.runSuspendTest
10+
import org.junit.jupiter.api.TestInstance
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
14+
/**
15+
* Tests for presigner
16+
*/
17+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
18+
class PollyPresignerTest {
19+
20+
@Test
21+
fun clientBasedPresign() = runSuspendTest {
22+
val request = SynthesizeSpeechRequest {
23+
voiceId = VoiceId.Salli
24+
outputFormat = OutputFormat.Pcm
25+
text = "hello world"
26+
}
27+
28+
val client = PollyClient { region = "us-east-1" }
29+
val presignedRequest = request.presign(client.config, 10)
30+
31+
CrtHttpEngine().use { engine ->
32+
val httpClient = sdkHttpClient(engine)
33+
34+
val call = httpClient.call(presignedRequest)
35+
call.complete()
36+
37+
assertEquals(200, call.response.status.value)
38+
}
39+
}
40+
}

services/s3/e2eTest/S3IntegrationTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package aws.sdk.kotlin.e2etest
66

77
import aws.sdk.kotlin.runtime.testing.runSuspendTest
88
import aws.sdk.kotlin.services.s3.S3Client
9-
import aws.sdk.kotlin.services.s3.model.*
9+
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
1010
import aws.smithy.kotlin.runtime.content.ByteStream
1111
import aws.smithy.kotlin.runtime.content.decodeToString
1212
import aws.smithy.kotlin.runtime.content.fromFile
@@ -22,19 +22,19 @@ import kotlin.time.Duration
2222
import kotlin.time.ExperimentalTime
2323

2424
/**
25-
* Tests for bucket operations
25+
* Tests for bucket operations and presigner
2626
*/
2727
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
2828
class S3BucketOpsIntegrationTest {
2929
companion object {
3030
const val DEFAULT_REGION = "us-east-2"
3131
}
3232

33-
val client = S3Client {
33+
private val client = S3Client {
3434
region = DEFAULT_REGION
3535
}
3636

37-
lateinit var testBucket: String
37+
private lateinit var testBucket: String
3838

3939
@BeforeAll
4040
private fun createResources(): Unit = runBlocking {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package aws.sdk.kotlin.e2etest
2+
3+
import aws.sdk.kotlin.runtime.http.engine.crt.CrtHttpEngine
4+
import aws.sdk.kotlin.runtime.testing.runSuspendTest
5+
import aws.sdk.kotlin.services.s3.S3Client
6+
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
7+
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
8+
import aws.sdk.kotlin.services.s3.presign
9+
import aws.smithy.kotlin.runtime.content.ByteStream
10+
import aws.smithy.kotlin.runtime.content.decodeToString
11+
import aws.smithy.kotlin.runtime.http.response.complete
12+
import aws.smithy.kotlin.runtime.http.sdkHttpClient
13+
import aws.smithy.kotlin.runtime.http.toByteStream
14+
import kotlinx.coroutines.runBlocking
15+
import org.junit.jupiter.api.AfterAll
16+
import org.junit.jupiter.api.BeforeAll
17+
import org.junit.jupiter.api.TestInstance
18+
import kotlin.test.Test
19+
import kotlin.test.assertEquals
20+
21+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
22+
class S3PresignerTest {
23+
companion object {
24+
const val DEFAULT_REGION = "us-east-2"
25+
}
26+
27+
private val client = S3Client {
28+
region = DEFAULT_REGION
29+
}
30+
31+
private lateinit var testBucket: String
32+
33+
@BeforeAll
34+
private fun createResources(): Unit = runBlocking {
35+
testBucket = S3TestUtils.getTestBucket(client)
36+
}
37+
38+
@AfterAll
39+
private fun cleanup() = runBlocking {
40+
S3TestUtils.deleteBucketAndAllContents(client, testBucket)
41+
}
42+
43+
@Test
44+
fun testPutObjectPresigner() = runSuspendTest {
45+
val contents = "presign-test"
46+
val keyName = "put-obj-from-memory-presigned.txt"
47+
48+
val presignedRequest = PutObjectRequest {
49+
bucket = testBucket
50+
key = keyName
51+
}.presign(client.config, 60)
52+
53+
S3TestUtils.responseCodeFromPut(presignedRequest, contents)
54+
55+
val req = GetObjectRequest {
56+
bucket = testBucket
57+
key = keyName
58+
}
59+
val roundTrippedContents = client.getObject(req) { it.body?.decodeToString() }
60+
61+
assertEquals(contents, roundTrippedContents)
62+
}
63+
64+
@Test
65+
fun testGetObjectPresigner() = runSuspendTest {
66+
val contents = "presign-test"
67+
val keyName = "put-obj-from-memory-presigned.txt"
68+
69+
client.putObject {
70+
bucket = testBucket
71+
key = keyName
72+
body = ByteStream.fromString(contents)
73+
}
74+
75+
val presignedRequest = GetObjectRequest {
76+
bucket = testBucket
77+
key = keyName
78+
}.presign(client.config, 60)
79+
80+
CrtHttpEngine().use { engine ->
81+
val httpClient = sdkHttpClient(engine)
82+
83+
val call = httpClient.call(presignedRequest)
84+
call.complete()
85+
86+
assertEquals(200, call.response.status.value)
87+
val body = call.response.body.toByteStream()?.decodeToString()
88+
assertEquals(contents, body)
89+
}
90+
}
91+
}

services/s3/e2eTest/S3TestUtils.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@
55
package aws.sdk.kotlin.e2etest
66

77
import aws.sdk.kotlin.services.s3.S3Client
8-
import aws.sdk.kotlin.services.s3.model.*
8+
import aws.sdk.kotlin.services.s3.model.BucketLocationConstraint
9+
import aws.sdk.kotlin.services.s3.model.ExpirationStatus
10+
import aws.sdk.kotlin.services.s3.model.LifecycleRule
11+
import aws.sdk.kotlin.services.s3.model.LifecycleRuleFilter
12+
import aws.sdk.kotlin.services.s3.model.NotFound
13+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
914
import kotlinx.coroutines.delay
1015
import kotlinx.coroutines.withTimeout
16+
import java.io.OutputStreamWriter
17+
import java.net.URL
1118
import java.util.*
19+
import javax.net.ssl.HttpsURLConnection
1220
import kotlin.time.Duration
1321
import kotlin.time.ExperimentalTime
14-
import kotlin.time.seconds
1522

1623
object S3TestUtils {
1724

@@ -94,4 +101,24 @@ object S3TestUtils {
94101
throw ex
95102
}
96103
}
104+
105+
fun responseCodeFromPut(presignedRequest: HttpRequest, content: String): Int {
106+
val url = URL(presignedRequest.url.toString())
107+
val connection: HttpsURLConnection = url.openConnection() as HttpsURLConnection
108+
presignedRequest.headers.forEach { key, values ->
109+
connection.setRequestProperty(key, values.first())
110+
}
111+
112+
connection.doOutput = true
113+
connection.requestMethod = "PUT"
114+
val out = OutputStreamWriter(connection.outputStream)
115+
out.write(content)
116+
out.close()
117+
118+
if (connection.errorStream != null) {
119+
error("request failed: ${connection.errorStream?.bufferedReader()?.readText()}")
120+
}
121+
122+
return connection.responseCode
123+
}
97124
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package aws.sdk.kotlin.services.sts
2+
3+
import aws.sdk.kotlin.runtime.http.engine.crt.CrtHttpEngine
4+
import aws.sdk.kotlin.runtime.testing.runSuspendTest
5+
import aws.sdk.kotlin.services.sts.model.GetCallerIdentityRequest
6+
import aws.smithy.kotlin.runtime.http.response.complete
7+
import aws.smithy.kotlin.runtime.http.sdkHttpClient
8+
import org.junit.jupiter.api.TestInstance
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
12+
/**
13+
* Tests for presigner
14+
*/
15+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
16+
class StsPresignerTest {
17+
18+
@Test
19+
fun testGetCallerIdentityPresigner() = runSuspendTest {
20+
val c = StsClient { region = "us-east-2" }
21+
val req = GetCallerIdentityRequest { }
22+
val presignedRequest = req.presign(c.config, 60)
23+
24+
CrtHttpEngine().use { engine ->
25+
val httpClient = sdkHttpClient(engine)
26+
27+
val call = httpClient.call(presignedRequest)
28+
call.complete()
29+
30+
assertEquals(200, call.response.status.value)
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)