diff --git a/src/main/java/software/amazon/cloudformation/LambdaWrapper.java b/src/main/java/software/amazon/cloudformation/LambdaWrapper.java index 5d1f7f9b..aa3f8276 100644 --- a/src/main/java/software/amazon/cloudformation/LambdaWrapper.java +++ b/src/main/java/software/amazon/cloudformation/LambdaWrapper.java @@ -44,6 +44,7 @@ import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.utils.StringUtils; import software.amazon.cloudformation.exceptions.BaseHandlerException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.FileScrubberException; import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.injection.CloudWatchLogsProvider; @@ -200,12 +201,19 @@ public void handleRequest(final InputStream inputStream, final OutputStream outp // deserialize incoming payload to modelled request try { request = this.serializer.deserialize(input, typeReference); + + handlerResponse = processInvocation(rawInput, request, context); } catch (MismatchedInputException e) { JSONObject resourceSchemaJSONObject = provideResourceSchemaJSONObject(); JSONObject rawModelObject = rawInput.getJSONObject("requestData").getJSONObject("resourceProperties"); + this.validator.validateObject(rawModelObject, resourceSchemaJSONObject); + + handlerResponse = ProgressEvent.defaultFailureHandler( + new CfnInvalidRequestException("Resource properties validation failed with invalid configuration", e), + HandlerErrorCode.InvalidRequest); } - handlerResponse = processInvocation(rawInput, request, context); + } catch (final ValidationException e) { String message; String fullExceptionMessage = ValidationException.buildFullExceptionMessage(e); @@ -221,7 +229,7 @@ public void handleRequest(final InputStream inputStream, final OutputStream outp } catch (final Throwable e) { // Exceptions are wrapped as a consistent error response to the caller (i.e; // CloudFormation) - e.printStackTrace(); // for root causing - logs to LambdaLogger by default + log(ExceptionUtils.getStackTrace(e)); // for root causing - logs to LambdaLogger by default handlerResponse = ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.InternalFailure); if (request != null && request.getRequestData() != null && MUTATING_ACTIONS.contains(request.getAction())) { handlerResponse.setResourceModel(request.getRequestData().getResourceProperties()); diff --git a/src/test/java/software/amazon/cloudformation/LambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/LambdaWrapperTest.java index c40c9a82..78d0d40a 100755 --- a/src/test/java/software/amazon/cloudformation/LambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/LambdaWrapperTest.java @@ -171,7 +171,7 @@ public void invokeHandler_nullResponse_returnsFailure(final String requestDataPa // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } verify(providerEventsLogger).refreshClient(); @@ -190,6 +190,36 @@ public void invokeHandler_nullResponse_returnsFailure(final String requestDataPa } } + @Test + public void invokeHandler_SchemaFailureOnNestedProperties() throws IOException { + // use actual validator to verify behaviour + final WrapperOverride wrapper = new WrapperOverride(providerLoggingCredentialsProvider, platformEventsLogger, + providerEventsLogger, providerMetricsPublisher, new Validator() { + }, httpClient); + + wrapper.setTransformResponse(resourceHandlerRequest); + + try (final InputStream in = loadRequestStream("create.request.with-extraneous-model-object.json"); + final OutputStream out = new ByteArrayOutputStream()) { + final Context context = getLambdaContext(); + + wrapper.handleRequest(in, out, context); + // validation failure metric should be published but no others + verify(providerMetricsPublisher).publishExceptionMetric(any(Instant.class), eq(Action.CREATE), any(Exception.class), + any(HandlerErrorCode.class)); + + // all metrics should be published, even for a single invocation + verify(providerMetricsPublisher).publishInvocationMetric(any(Instant.class), eq(Action.CREATE)); + + // verify initialiseRuntime was called and initialised dependencies + verifyInitialiseRuntime(); + + // verify output response + verifyHandlerResponse(out, ProgressEvent.builder().errorCode(HandlerErrorCode.InvalidRequest) + .status(OperationStatus.FAILED).message("Resource properties validation failed with invalid configuration").build()); + } + } + @Test public void invokeHandlerForCreate_without_customer_loggingCredentials() throws IOException { invokeHandler_without_customerLoggingCredentials("create.request-without-logging-credentials.json", Action.CREATE); @@ -224,7 +254,7 @@ private void invokeHandler_without_customerLoggingCredentials(final String reque // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } verifyNoMoreInteractions(providerEventsLogger); @@ -259,7 +289,7 @@ public void invokeHandler_handlerFailed_returnsFailure(final String requestDataP // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } // verify output response @@ -318,7 +348,7 @@ public void invokeHandler_CompleteSynchronously_returnsSuccess(final String requ // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } // verify output response @@ -467,7 +497,7 @@ public void reInvokeHandler_InProgress_returnsInProgress(final String requestDat // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } // verify output response @@ -481,6 +511,7 @@ public void reInvokeHandler_InProgress_returnsInProgress(final String requestDat public void invokeHandler_SchemaValidationFailure(final String requestDataPath, final String actionAsString) throws IOException { final Action action = Action.valueOf(actionAsString); + doThrow(ValidationException.class).when(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); wrapper.setTransformResponse(resourceHandlerRequest); @@ -502,7 +533,7 @@ public void invokeHandler_SchemaValidationFailure(final String requestDataPath, // verify that model validation occurred for CREATE/UPDATE/DELETE if (action == Action.CREATE || action == Action.UPDATE || action == Action.DELETE) { - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); } // verify output response @@ -548,7 +579,6 @@ providerEventsLogger, providerMetricsPublisher, new Validator() { final Context context = getLambdaContext(); wrapper.handleRequest(in, out, context); - // validation failure metric should be published but no others verify(providerMetricsPublisher, times(1)).publishExceptionMetric(any(Instant.class), eq(Action.CREATE), any(Exception.class), any(HandlerErrorCode.class)); @@ -711,7 +741,7 @@ public void invokeHandler_throwsAmazonServiceException_returnsServiceException() any(AmazonServiceException.class), any(HandlerErrorCode.class)); // verify that model validation occurred for CREATE/UPDATE/DELETE - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); // verify output response verifyHandlerResponse(out, @@ -728,7 +758,7 @@ public void invokeHandler_throwsSDK2ServiceException_returnsServiceException() t wrapper.setTransformResponse(resourceHandlerRequest); try (final InputStream in = loadRequestStream("create.request.json"); - final OutputStream out = new ByteArrayOutputStream()) { + final OutputStream out = new ByteArrayOutputStream()) { final Context context = getLambdaContext(); wrapper.handleRequest(in, out, context); @@ -745,7 +775,7 @@ public void invokeHandler_throwsSDK2ServiceException_returnsServiceException() t any(CloudWatchLogsException.class), any(HandlerErrorCode.class)); // verify that model validation occurred for CREATE/UPDATE/DELETE - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); // verify output response verifyHandlerResponse(out, @@ -780,7 +810,7 @@ public void invokeHandler_throwsResourceAlreadyExistsException_returnsAlreadyExi any(ResourceAlreadyExistsException.class), any(HandlerErrorCode.class)); // verify that model validation occurred for CREATE/UPDATE/DELETE - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); // verify output response verifyHandlerResponse(out, @@ -815,7 +845,7 @@ public void invokeHandler_throwsResourceNotFoundException_returnsNotFound() thro any(ResourceNotFoundException.class), any(HandlerErrorCode.class)); // verify that model validation occurred for CREATE/UPDATE/DELETE - verify(validator, times(1)).validateObject(any(JSONObject.class), any(JSONObject.class)); + verify(validator).validateObject(any(JSONObject.class), any(JSONObject.class)); // verify output response verifyHandlerResponse(out, diff --git a/src/test/java/software/amazon/cloudformation/data/create.request.with-extraneous-model-object.json b/src/test/java/software/amazon/cloudformation/data/create.request.with-extraneous-model-object.json new file mode 100644 index 00000000..699bf0e9 --- /dev/null +++ b/src/test/java/software/amazon/cloudformation/data/create.request.with-extraneous-model-object.json @@ -0,0 +1,43 @@ +{ + "awsAccountId": "123456789012", + "bearerToken": "123456", + "region": "us-east-1", + "action": "CREATE", + "responseEndpoint": "https://cloudformation.us-west-2.amazonaws.com", + "resourceType": "AWS::Test::TestModel", + "resourceTypeVersion": "1.0", + "requestContext": {}, + "requestData": { + "callerCredentials": { + "accessKeyId": "IASAYK835GAIFHAHEI23", + "secretAccessKey": "66iOGPN5LnpZorcLr8Kh25u8AbjHVllv5/poh2O0", + "sessionToken": "lameHS2vQOknSHWhdFYTxm2eJc1JMn9YBNI4nV4mXue945KPL6DHfW8EsUQT5zwssYEC1NvYP9yD6Y5s5lKR3chflOHPFsIe6eqg" + }, + "providerCredentials": { + "accessKeyId": "HDI0745692Y45IUTYR78", + "secretAccessKey": "4976TUYVI234/5GW87ERYG823RF87GY9EIUH452I3", + "sessionToken": "842HYOFIQAEUDF78R8T7IU43HSADYGIFHBJSDHFA87SDF9PYvN1CEYASDUYFT5TQ97YASIHUDFAIUEYRISDKJHFAYSUDTFSDFADS" + }, + "providerLogGroupName": "providerLoggingGroupName", + "logicalResourceId": "myBucket", + "resourceProperties": { + "property1": "abc", + "property2": 123, + "property3": { + "subProperty": { + "propertyArray": "singleValue" + } + } + }, + "systemTags": { + "aws:cloudformation:stack-id": "SampleStack" + }, + "stackTags": { + "tag1": "abc" + }, + "previousStackTags": { + "tag1": "def" + } + }, + "stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968" +} diff --git a/src/test/resources/software/amazon/cloudformation/wrapper-override.json b/src/test/resources/software/amazon/cloudformation/wrapper-override.json index d2ebeaa3..2a40593b 100644 --- a/src/test/resources/software/amazon/cloudformation/wrapper-override.json +++ b/src/test/resources/software/amazon/cloudformation/wrapper-override.json @@ -1,12 +1,33 @@ { "typeName": "Test::Resource::Type", "description": "Description", + "definitions": { + "subProperty": { + "type": "object", + "properties": { + "propertyArray": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "properties": { "property1": { "type": "string" }, "property2": { "type": "integer" + }, + "property3": { + "type": "object", + "properties": { + "subProperty": { + "$ref": "#/definitions/subProperty" + } + } } }, "additionalProperties": false,