Skip to content

Commit e23c843

Browse files
authored
fix(codegen): Fix usage of unicode in bucket names of s3 presigner (#487)
1 parent 5b91a0e commit e23c843

File tree

4 files changed

+900
-87
lines changed

4 files changed

+900
-87
lines changed

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,28 @@ import aws.sdk.kotlin.crt.http.HttpRequest as CrtHttpRequest
2929
/**
3030
* The service configuration details for a presigned request
3131
*
32-
* @property region The AWS region to which the request is going.
33-
* @property signingName The signing name used to sign the request.
34-
* @property serviceId the service id used to sign the request.
35-
* @property endpointResolver Resolves the endpoint to determine where the request should be sent.
36-
* @property credentialsProvider Resolves credentials to sign the request with.
32+
* @property region The AWS region to which the request is going
33+
* @property signingName The signing name used to sign the request
34+
* @property serviceId the service id used to sign the request
35+
* @property endpointResolver Resolves the endpoint to determine where the request should be sent
36+
* @property credentialsProvider Resolves credentials to sign the request with
37+
* @property useDoubleUriEncode Determines if presigner should double encode Uri
38+
* @property normalizeUriPath Determines if presigned URI path will be normalized
3739
*/
3840
public interface ServicePresignConfig {
3941
public val region: String
4042
public val signingName: String
4143
public val serviceId: String
4244
public val endpointResolver: AwsEndpointResolver
4345
public val credentialsProvider: CredentialsProvider
46+
public val useDoubleUriEncode: Boolean
47+
public val normalizeUriPath: Boolean
4448
}
4549

4650
/**
4751
* Where the signature is placed in the presigned request
48-
* @property HEADER Signing details to be placed in a header.
49-
* @property QUERY_STRING Signing details to be added to the query string.
52+
* @property HEADER Signing details to be placed in a header
53+
* @property QUERY_STRING Signing details to be added to the query string
5054
*/
5155
public enum class SigningLocation {
5256
HEADER,
@@ -58,7 +62,7 @@ public enum class SigningLocation {
5862
* @property method HTTP method of the presigned request
5963
* @property path HTTP path of the presigned request
6064
* @property queryString the HTTP querystring of the presigned request
61-
* @property durationSeconds Number of seconds that the request will be valid for after being signed.
65+
* @property durationSeconds Number of seconds that the request will be valid for after being signed
6266
* @property signBody Specifies if the request body should be signed
6367
* @property signingLocation Specifies where the signing information should be placed in the presigned request
6468
* @property additionalHeaders Custom headers that should be signed as part of the request
@@ -74,10 +78,10 @@ public data class PresignedRequestConfig(
7478
)
7579

7680
/**
77-
* Generate a presigned request given the service and operation configurations.
81+
* Generate a presigned request given the service and operation configurations
7882
* @param serviceConfig The service configuration to use in signing the request
7983
* @param requestConfig The presign configuration to use in signing the request
80-
* @return a [HttpRequest] that can be executed by any HTTP client within the specified duration.
84+
* @return a [HttpRequest] that can be executed by any HTTP client within the specified duration
8185
*/
8286
@InternalSdkApi
8387
public suspend fun createPresignedRequest(serviceConfig: ServicePresignConfig, requestConfig: PresignedRequestConfig): HttpRequest {
@@ -92,6 +96,8 @@ public suspend fun createPresignedRequest(serviceConfig: ServicePresignConfig, r
9296
signedBodyHeader = AwsSignedBodyHeaderType.X_AMZ_CONTENT_SHA256
9397
signedBodyValue = if (requestConfig.signBody) null else AwsSignedBodyValue.UNSIGNED_PAYLOAD
9498
expirationInSeconds = requestConfig.durationSeconds
99+
useDoubleUriEncode = serviceConfig.useDoubleUriEncode
100+
normalizeUriPath = serviceConfig.normalizeUriPath
95101
}
96102

97103
val unsignedUrl = Url(

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

Lines changed: 86 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ data class PresignableOperation(
5656
* This integration applies to any AWS service that provides presign capability on one or more operations.
5757
*/
5858
class PresignerGenerator : KotlinIntegration {
59-
6059
/**
61-
* Identifies the [PresignConfigFn] section for overriding generated implementation.
60+
* Identifies the [PresignConfigFnSection] section for overriding generated implementation.
6261
*/
6362
object PresignConfigFnSection : SectionId {
6463
const val CodegenContext = "CodegenContext"
@@ -183,67 +182,25 @@ class PresignerGenerator : KotlinIntegration {
183182
}
184183

185184
// Generate presign config builder
185+
val clientProperties: List<ClientConfigProperty> = getClientProperties(sigv4ServiceName, serviceShape.sdkId)
186186
val rc = RenderingContext(writer, serviceShape, ctx.model, ctx.symbolProvider, ctx.settings)
187-
renderPresignConfigBuilder(writer, presignConfigTypeName, sigv4ServiceName, serviceShape.sdkId, rc)
187+
renderPresignConfigBuilder(writer, presignConfigTypeName, rc, clientProperties)
188188
}
189189

190-
private fun renderPresignConfigBuilder(writer: KotlinWriter, presignConfigTypeName: String, sigv4ServiceName: String, serviceId: String, renderingContext: RenderingContext<ServiceShape>) {
190+
private fun renderPresignConfigBuilder(writer: KotlinWriter, presignConfigTypeName: String, renderingContext: RenderingContext<ServiceShape>, clientProperties: List<ClientConfigProperty>) {
191191
writer.dokka {
192192
write("Provides a subset of the service client configuration necessary to presign a request.")
193193
write("This type can be used to presign requests in cases where an existing service client")
194194
write("instance is not available.")
195195
}
196196
writer.addImport(AwsRuntimeTypes.Core.ClientException)
197197
writer.putContext("configClass.name", presignConfigTypeName)
198-
val credentialsProviderProperty = ClientConfigProperty {
199-
symbol = AwsRuntimeTypes.Types.CredentialsProvider
200-
name = "credentialsProvider"
201-
documentation = "The AWS credentials provider to use for authenticating requests. If not provided a [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used."
202-
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
203-
propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultChainCredentialsProvider()")
204-
}
205-
val endpointResolverProperty = ClientConfigProperty {
206-
symbol = AwsRuntimeTypes.Endpoint.AwsEndpointResolver
207-
name = "endpointResolver"
208-
documentation = "Determines the endpoint (hostname) to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option."
209-
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
210-
propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultEndpointResolver()")
211-
}
212-
val region = ClientConfigProperty {
213-
symbol = buildSymbol {
214-
name = "String"
215-
namespace = "kotlin"
216-
nullable = true
217-
}
218-
name = "region"
219-
documentation = "AWS region to make requests for"
220-
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
221-
propertyType = ClientConfigPropertyType.Required()
222-
}
223-
val signingNameProperty = ClientConfigProperty {
224-
symbol = KotlinTypes.String
225-
name = "signingName"
226-
documentation = "Service identifier used to sign requests"
227-
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
228-
propertyType = ClientConfigPropertyType.ConstantValue(sigv4ServiceName.dq())
229-
}
230-
val serviceIdProperty = ClientConfigProperty {
231-
symbol = KotlinTypes.String
232-
name = "serviceId"
233-
documentation = "Service identifier used to resolve endpoints"
234-
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
235-
propertyType = ClientConfigPropertyType.ConstantValue(serviceId.dq())
236-
}
237198

238199
val ccg = ClientConfigGenerator(
239200
renderingContext,
240201
false,
241202
AwsRuntimeTypes.Signing.ServicePresignConfig,
242-
credentialsProviderProperty,
243-
endpointResolverProperty,
244-
region,
245-
signingNameProperty,
246-
serviceIdProperty
203+
*clientProperties.toTypedArray()
247204
)
248205
ccg.render()
249206
}
@@ -351,4 +308,85 @@ class PresignerGenerator : KotlinIntegration {
351308
write("return createPresignedRequest(presignConfig, $requestConfigFnName(this, durationSeconds))")
352309
}
353310
}
311+
312+
// Provide all client properties for a presigner client
313+
private fun getClientProperties(sigv4ServiceName: String, serviceId: String): List<ClientConfigProperty> =
314+
listOf(
315+
ClientConfigProperty {
316+
symbol = AwsRuntimeTypes.Types.CredentialsProvider
317+
name = "credentialsProvider"
318+
documentation = "The AWS credentials provider to use for authenticating requests. If not provided a [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used."
319+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
320+
propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultChainCredentialsProvider()")
321+
},
322+
ClientConfigProperty {
323+
symbol = AwsRuntimeTypes.Endpoint.AwsEndpointResolver
324+
name = "endpointResolver"
325+
documentation = "Determines the endpoint (hostname) to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option."
326+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
327+
propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultEndpointResolver()")
328+
},
329+
ClientConfigProperty {
330+
symbol = buildSymbol {
331+
name = "String"
332+
namespace = "kotlin"
333+
nullable = true
334+
}
335+
name = "region"
336+
documentation = "AWS region to make requests for"
337+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
338+
propertyType = ClientConfigPropertyType.Required()
339+
},
340+
ClientConfigProperty {
341+
symbol = KotlinTypes.String
342+
name = "signingName"
343+
documentation = "Service identifier used to sign requests"
344+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
345+
propertyType = ClientConfigPropertyType.ConstantValue(sigv4ServiceName.dq())
346+
},
347+
ClientConfigProperty {
348+
symbol = KotlinTypes.String
349+
name = "serviceId"
350+
documentation = "Service identifier used to resolve endpoints"
351+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
352+
propertyType = ClientConfigPropertyType.ConstantValue(serviceId.dq())
353+
},
354+
ClientConfigProperty {
355+
symbol = buildSymbol {
356+
name = "Boolean"
357+
namespace = "kotlin"
358+
nullable = true
359+
}
360+
name = "useDoubleUriEncode"
361+
documentation = "Determines if presigner should double encode Uri"
362+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
363+
propertyType = ClientConfigPropertyType.ConstantValue(useDoubleUriEncodeValueForService(serviceId))
364+
},
365+
ClientConfigProperty {
366+
symbol = buildSymbol {
367+
name = "Boolean"
368+
namespace = "kotlin"
369+
nullable = true
370+
}
371+
name = "normalizeUriPath"
372+
documentation = "Determines if presigned URI path will be normalized"
373+
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
374+
propertyType = ClientConfigPropertyType.ConstantValue(normalizeUriPathValueForService(serviceId))
375+
}
376+
)
377+
378+
// Determine useDoubleUriEncode setting based on service
379+
// From https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html:
380+
// "Each path segment must be URI-encoded twice (except for Amazon S3 which only gets URI-encoded once)."
381+
private fun useDoubleUriEncodeValueForService(serviceId: String): String =
382+
when (serviceId) {
383+
"S3" -> false
384+
else -> true
385+
}.toString()
386+
387+
// Determine normalizeUriPath setting based on service
388+
// From https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request:
389+
// "You do not normalize URI paths for requests to Amazon S3. For example, you may have a bucket with an object named "my-object//example//photo.user". Normalizing the path changes the object name in the request to "my-object/example/photo.user". This is an incorrect path for that object."
390+
private fun normalizeUriPathValueForService(serviceId: String): String =
391+
useDoubleUriEncodeValueForService(serviceId)
354392
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,11 @@ class PresignerGeneratorTest {
227227
class TestPresignConfig private constructor(builder: Builder): ServicePresignConfig {
228228
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider()
229229
override val endpointResolver: AwsEndpointResolver = builder.endpointResolver ?: DefaultEndpointResolver()
230+
override val normalizeUriPath: Boolean = true
230231
override val region: String = requireNotNull(builder.region) { "region is a required configuration property" }
231232
override val serviceId: String = "example"
232233
override val signingName: String = "example-signing-name"
234+
override val useDoubleUriEncode: Boolean = true
233235
companion object {
234236
inline operator fun invoke(block: Builder.() -> kotlin.Unit): ServicePresignConfig = Builder().apply(block).build()
235237
}

0 commit comments

Comments
 (0)