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..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 @@ -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("Envelope not found in the object"); + } } catch (Exception e) { throw new ValidationException("Cannot find envelope <"+envelope+"> in the object <"+obj+">", e); } 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..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 @@ -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,8 @@ */ @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 +63,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 +111,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()); } } } @@ -131,7 +137,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..9eb96c0e8 --- /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/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 c8741f1e3..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 @@ -101,6 +101,24 @@ 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_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()); 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