From 3c8019a6ae8b7426d278a2eabae1443254b145bb Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Tue, 25 Oct 2022 14:34:12 +0200 Subject: [PATCH 1/5] add precedence for the envelope over built-in types --- .../validation/internal/ValidationAspect.java | 17 ++++++++--- .../SQSWithCustomEnvelopeHandler.java | 28 +++++++++++++++++++ .../internal/ValidationAspectTest.java | 9 ++++++ .../src/test/resources/sqs_message.json | 22 +++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java create mode 100644 powertools-validation/src/test/resources/sqs_message.json diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 0a1f00599..6e35532a5 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -19,6 +19,8 @@ import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; @@ -36,6 +38,9 @@ */ @Aspect public class ValidationAspect { + private static final Logger LOG = LoggerFactory.getLogger(ValidationAspect.class); + + @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(validation)") public void callAt(Validation validation) { @@ -59,7 +64,9 @@ && placedOnRequestHandler(pjp)) { JsonSchema inboundJsonSchema = getJsonSchema(validation.inboundSchema(), true); Object obj = pjp.getArgs()[0]; - if (obj instanceof APIGatewayProxyRequestEvent) { + if (validation.envelope() != null && !validation.envelope().isEmpty()) { + validate(obj, inboundJsonSchema, validation.envelope()); + } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; validate(event.getBody(), inboundJsonSchema); } else if (obj instanceof APIGatewayV2HTTPEvent) { @@ -105,7 +112,7 @@ && placedOnRequestHandler(pjp)) { KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else { - validate(obj, inboundJsonSchema, validation.envelope()); + LOG.warn("Unhandled event type {}, please use the 'envelope' parameter to specify what to validate", obj.getClass().getName()); } } } @@ -115,7 +122,9 @@ && placedOnRequestHandler(pjp)) { if (validationNeeded && !validation.outboundSchema().isEmpty()) { JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); - if (result instanceof APIGatewayProxyResponseEvent) { + if (validation.envelope() != null && !validation.envelope().isEmpty()) { + validate(result, outboundJsonSchema, validation.envelope()); + } else if (result instanceof APIGatewayProxyResponseEvent) { APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; validate(response.getBody(), outboundJsonSchema); } else if (result instanceof APIGatewayV2HTTPResponse) { @@ -131,7 +140,7 @@ && placedOnRequestHandler(pjp)) { KinesisAnalyticsInputPreprocessingResponse response = (KinesisAnalyticsInputPreprocessingResponse) result; response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); } else { - validate(result, outboundJsonSchema, validation.envelope()); + LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", result.getClass().getName()); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java new file mode 100644 index 000000000..0b1a38c04 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import software.amazon.lambda.powertools.validation.Validation; + +public class SQSWithCustomEnvelopeHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json", envelope = "records[*].powertools_json(body).powertools_json(Message)") + public String handleRequest(SQSEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index c8741f1e3..5f09d84ff 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -101,6 +101,15 @@ public void validate_SQS() { assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } + @Test + public void validate_SQS_CustomEnvelopeTakePrecedence() { + PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); + + SQSWithCustomEnvelopeHandler handler = new SQSWithCustomEnvelopeHandler(); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } + @Test public void validate_Kinesis() { PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); diff --git a/powertools-validation/src/test/resources/sqs_message.json b/powertools-validation/src/test/resources/sqs_message.json new file mode 100644 index 000000000..068279b74 --- /dev/null +++ b/powertools-validation/src/test/resources/sqs_message.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db2", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"Message\": \"{\\n \\\"id\\\": 43242,\\n \\\"name\\\": \\\"FooBar XY\\\",\\n \\\"price\\\": 258\\n}\"}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file From ee7b51221b8eddfb5b8255b84472f3f071e16b3f Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Tue, 25 Oct 2022 14:44:34 +0200 Subject: [PATCH 2/5] cannot use same envelope for in and out --- .../powertools/validation/internal/ValidationAspect.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 6e35532a5..60707cfbe 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -122,9 +122,7 @@ && placedOnRequestHandler(pjp)) { if (validationNeeded && !validation.outboundSchema().isEmpty()) { JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); - if (validation.envelope() != null && !validation.envelope().isEmpty()) { - validate(result, outboundJsonSchema, validation.envelope()); - } else if (result instanceof APIGatewayProxyResponseEvent) { + if (result instanceof APIGatewayProxyResponseEvent) { APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; validate(response.getBody(), outboundJsonSchema); } else if (result instanceof APIGatewayV2HTTPResponse) { From 4bd8305fedbf97e4966e0846cf947232b80d7e9a Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Tue, 25 Oct 2022 16:14:47 +0200 Subject: [PATCH 3/5] remove extra new line --- .../lambda/powertools/validation/internal/ValidationAspect.java | 1 - 1 file changed, 1 deletion(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 60707cfbe..a9d43271b 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -40,7 +40,6 @@ public class ValidationAspect { private static final Logger LOG = LoggerFactory.getLogger(ValidationAspect.class); - @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(validation)") public void callAt(Validation validation) { From e54c4d99783e913baac93a5ec65af3346b4a253d Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 27 Oct 2022 11:50:23 +0200 Subject: [PATCH 4/5] handle event field names properly ex: SQS Records with big R --- powertools-validation/pom.xml | 10 +++---- .../validation/ValidationUtils.java | 12 +++++++- .../SQSWithCustomEnvelopeHandler.java | 2 +- .../handlers/SQSWithWrongEnvelopeHandler.java | 29 +++++++++++++++++++ .../internal/ValidationAspectTest.java | 9 ++++++ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 61beb34ba..c5a0a767c 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -75,6 +75,10 @@ json-schema-validator 1.0.73 + + com.amazonaws + aws-lambda-java-serialization + @@ -87,11 +91,7 @@ junit-jupiter-engine test - - com.amazonaws - aws-lambda-java-serialization - test - + org.apache.commons commons-lang3 diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java index 83f34ebfd..45c135c1b 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java @@ -13,6 +13,7 @@ */ package software.amazon.lambda.powertools.validation; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -21,9 +22,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.NullNode; import com.networknt.schema.JsonSchema; import com.networknt.schema.ValidationMessage; import io.burt.jmespath.Expression; @@ -65,9 +69,15 @@ public static void validate(Object obj, JsonSchema jsonSchema, String envelope) } JsonNode subNode; try { - JsonNode jsonNode = ValidationConfig.get().getObjectMapper().valueToTree(obj); + PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(obj.getClass(), ClassLoader.getSystemClassLoader()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + pojoSerializer.toJson(obj, out); + JsonNode jsonNode = ValidationConfig.get().getObjectMapper().readTree(out.toString("UTF-8")); Expression expression = ValidationConfig.get().getJmesPath().compile(envelope); subNode = expression.search(jsonNode); + if (subNode == null || subNode instanceof NullNode) { + throw new ValidationException("Not found"); + } } catch (Exception e) { throw new ValidationException("Cannot find envelope <"+envelope+"> in the object <"+obj+">", e); } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java index 0b1a38c04..9eb96c0e8 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java @@ -21,7 +21,7 @@ public class SQSWithCustomEnvelopeHandler implements RequestHandler { @Override - @Validation(inboundSchema = "classpath:/schema_v7.json", envelope = "records[*].powertools_json(body).powertools_json(Message)") + @Validation(inboundSchema = "classpath:/schema_v7.json", envelope = "Records[*].powertools_json(body).powertools_json(Message)") public String handleRequest(SQSEvent input, Context context) { return "OK"; } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java new file mode 100644 index 000000000..c1859f29a --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import software.amazon.lambda.powertools.validation.Validation; + +public class SQSWithWrongEnvelopeHandler implements RequestHandler { + + @Override + // real event contains Records with big R (https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) + @Validation(inboundSchema = "classpath:/schema_v7.json", envelope = "records[*].powertools_json(body).powertools_json(Message)") + public String handleRequest(SQSEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 5f09d84ff..2c66885d6 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -110,6 +110,15 @@ public void validate_SQS_CustomEnvelopeTakePrecedence() { assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } + @Test + public void validate_SQS_WrongEnvelope_shouldThrowValidationException() { + PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); + + SQSWithWrongEnvelopeHandler handler = new SQSWithWrongEnvelopeHandler(); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + } + @Test public void validate_Kinesis() { PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); From 18640508933addf26541c42e281252844f503834 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Wed, 2 Nov 2022 11:32:14 +0100 Subject: [PATCH 5/5] more explicit exception message --- .../amazon/lambda/powertools/validation/ValidationUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java index 45c135c1b..12c51c632 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java @@ -76,7 +76,7 @@ public static void validate(Object obj, JsonSchema jsonSchema, String envelope) Expression expression = ValidationConfig.get().getJmesPath().compile(envelope); subNode = expression.search(jsonNode); if (subNode == null || subNode instanceof NullNode) { - throw new ValidationException("Not found"); + throw new ValidationException("Envelope not found in the object"); } } catch (Exception e) { throw new ValidationException("Cannot find envelope <"+envelope+"> in the object <"+obj+">", e);