Skip to content

feat: sqs md5 checksum validation #1544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions .changes/cc154f8b-62ba-4ab5-8da5-84d1b5fe0d9f.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "cc154f8b-62ba-4ab5-8da5-84d1b5fe0d9f",
"type": "feature",
"description": "Added MD5 checksum validation for SQS message operations",
"issues": [
"awslabs/aws-sdk-kotlin#222"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,29 @@ public inline fun <reified T : Enum<T>> AwsProfile.getEnumOrNull(key: String, su
it.name.equals(value, ignoreCase = true)
} ?: throw ConfigurationException(
buildString {
append(key)
append(" '")
append(value)
append("' is not supported, should be one of: ")
append("$key '$value' is not supported, should be one of: ")
enumValues<T>().joinTo(this) { it.name.lowercase() }
},
)
}

/**
* Parse a config value as an enum set.
*/
@InternalSdkApi
public inline fun <reified T : Enum<T>> AwsProfile.getEnumSetOrNull(key: String, subKey: String? = null): Set<T>? =
getOrNull(key, subKey)?.split(",")?.map { it ->
val value = it.trim()
enumValues<T>().firstOrNull { enumValue ->
enumValue.name.equals(value, ignoreCase = true)
} ?: throw ConfigurationException(
buildString {
append("$key '$value' is not supported, should be one of: ")
enumValues<T>().joinTo(this) { it.name.lowercase() }
},
)
}?.toSet()

internal fun AwsProfile.getUrlOrNull(key: String, subKey: String? = null): Url? =
getOrNull(key, subKey)?.let {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.codegen.customization.sqs

import aws.sdk.kotlin.codegen.ServiceClientCompanionObjectWriter
import aws.sdk.kotlin.codegen.sdkId
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
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.buildSymbol
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape

/**
* Register interceptor to handle SQS message MD5 checksum validation.
*/
class SqsMd5ChecksumValidationIntegration : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
model.expectShape<ServiceShape>(settings.service).sdkId.lowercase() == "sqs"

companion object {
val ValidationEnabledProp: ConfigProperty = ConfigProperty {
name = "checksumValidationEnabled"
symbol = buildSymbol {
name = "ValidationEnabled"
namespace = "aws.sdk.kotlin.services.sqs.internal"
nullable = false
}
propertyType = ConfigPropertyType.Custom(
render = { prop, writer ->
writer.write("public val #1L: #2T = builder.#1L ?: #2T.NEVER", prop.propertyName, prop.symbol)
},
renderBuilder = { prop, writer ->
prop.documentation?.let(writer::dokka)
writer.write("public var #L: #T? = null", prop.propertyName, prop.symbol)
writer.write("")
},
)
documentation = """
Specifies when MD5 checksum validation should be performed for SQS messages. This controls the automatic
calculation and validation of checksums during message operations.

Valid values:
- `ALWAYS` - Checksums are calculated and validated for both sending and receiving operations
(SendMessage, SendMessageBatch, and ReceiveMessage)
- `WHEN_SENDING` - Checksums are only calculated and validated during send operations
(SendMessage and SendMessageBatch)
- `WHEN_RECEIVING` - Checksums are only calculated and validated during receive operations
(ReceiveMessage)
- `NEVER` (default) - No checksum calculation or validation is performed
""".trimIndent()
// TODO: MD5 checksum validation is temporarily disabled. Change default to ALWAYS in v1.5
}

private val validationScope = buildSymbol {
name = "ValidationScope"
namespace = "aws.sdk.kotlin.services.sqs.internal"
nullable = false
}

val ValidationScopeProp: ConfigProperty = ConfigProperty {
name = "checksumValidationScopes"
symbol = KotlinTypes.Collections.set(validationScope)
propertyType = ConfigPropertyType.Custom(
render = { prop, writer ->
writer.write("public val #1L: #2T = builder.#1L ?: #3T.entries.toSet()", prop.propertyName, prop.symbol, validationScope)
},
renderBuilder = { prop, writer ->
prop.documentation?.let(writer::dokka)
writer.write("public var #L: #T? = null", prop.propertyName, prop.symbol)
writer.write("")
},
)
documentation = """
Specifies which parts of an SQS message should undergo MD5 checksum validation. This configuration
accepts a set of validation scopes that determine which message components to validate.

Valid values:
- `MESSAGE_ATTRIBUTES` - Validates checksums for message attributes
- `MESSAGE_SYSTEM_ATTRIBUTES` - Validates checksums for message system attributes
(Note: Not available for ReceiveMessage operations as SQS does not calculate checksums for
system attributes during message receipt)
- `MESSAGE_BODY` - Validates checksums for the message body

Default: All three scopes (`MESSAGE_ATTRIBUTES`, `MESSAGE_SYSTEM_ATTRIBUTES`, `MESSAGE_BODY`)
""".trimIndent()
}
}

override fun additionalServiceConfigProps(ctx: CodegenContext): List<ConfigProperty> =
listOf(
ValidationEnabledProp,
ValidationScopeProp,
)

override val sectionWriters: List<SectionWriterBinding>
get() = listOf(
SectionWriterBinding(
ServiceClientCompanionObjectWriter.FinalizeEnvironmentalConfig,
finalizeSqsConfigWriter,
),
)

// add SQS-specific config finalization
private val finalizeSqsConfigWriter = AppendingSectionWriter { writer ->
val finalizeSqsConfig = buildSymbol {
name = "finalizeSqsConfig"
namespace = "aws.sdk.kotlin.services.sqs.internal"
}
writer.write("#T(builder, sharedConfig)", finalizeSqsConfig)
}

override fun customizeMiddleware(
ctx: ProtocolGenerator.GenerationContext,
resolved: List<ProtocolMiddleware>,
): List<ProtocolMiddleware> = resolved + listOf(SqsMd5ChecksumValidationMiddleware)
}

internal object SqsMd5ChecksumValidationMiddleware : ProtocolMiddleware {
override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = when (op.id.name) {
"ReceiveMessage",
"SendMessage",
"SendMessageBatch",
-> true
else -> false
}

override val name: String = "SqsMd5ChecksumValidationInterceptor"

override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {
val symbol = buildSymbol {
name = [email protected]
namespace = "aws.sdk.kotlin.services.sqs"
}

writer.write("op.interceptors.add(#T(config.checksumValidationEnabled, config.checksumValidationScopes))", symbol)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ aws.sdk.kotlin.codegen.smoketests.SmokeTestsDenyListIntegration
aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestSuccessHttpEngineIntegration
aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestFailHttpEngineIntegration
aws.sdk.kotlin.codegen.customization.AwsQueryModeCustomization
aws.sdk.kotlin.codegen.customization.sqs.SqsMd5ChecksumValidationIntegration
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.codegen.customization.sqs

import aws.sdk.kotlin.codegen.testutil.model
import org.junit.jupiter.api.Test
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
import software.amazon.smithy.kotlin.codegen.test.defaultSettings
import software.amazon.smithy.kotlin.codegen.test.newTestContext
import software.amazon.smithy.model.shapes.OperationShape
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail

class SqsMd5ChecksumValidationIntegrationTest {
object FooMiddleware : ProtocolMiddleware {
override val name: String = "FooMiddleware"
override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) =
fail("Unexpected call to `FooMiddleware.render`")
}

@Test
fun testNotExpectedForNonSqsModel() {
val model = model("NotSqs")
val actual = SqsMd5ChecksumValidationIntegration().enabledForService(model, model.defaultSettings())

assertFalse(actual)
}

@Test
fun testExpectedForSqsModel() {
val model = model("Sqs")
val actual = SqsMd5ChecksumValidationIntegration().enabledForService(model, model.defaultSettings())

assertTrue(actual)
}

@Test
fun testMiddlewareAddition() {
val model = model("Sqs")
val preexistingMiddleware = listOf(FooMiddleware)
val ctx = model.newTestContext("Sqs")
val actual = SqsMd5ChecksumValidationIntegration().customizeMiddleware(ctx.generationCtx, preexistingMiddleware)

assertEquals(listOf(FooMiddleware, SqsMd5ChecksumValidationMiddleware), actual)
}
}
Loading
Loading