Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/main/java/software/amazon/cloudformation/LambdaWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand Down
54 changes: 42 additions & 12 deletions src/test/java/software/amazon/cloudformation/LambdaWrapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.<TestModel, TestContext>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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
{
"typeName": "Test::Resource::Type",
"description": "Description",
"definitions": {
"subProperty": {
"type": "object",
"properties": {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I think the whole problem is that the schema doesn't define "additionalProperties": false for the nest property. In such way, the deserialization to POJO fails due to this mismatch. However, validation pass as "additionalProperties": false is missing and validation will skip this additional properties.

The resource should fix the schema in such cases. But of course, this fix is still valuable.

"propertyArray": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"properties": {
"property1": {
"type": "string"
},
"property2": {
"type": "integer"
},
"property3": {
"type": "object",
"properties": {
"subProperty": {
"$ref": "#/definitions/subProperty"
}
}
}
},
"additionalProperties": false,
Expand Down