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
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import software.amazon.cloudformation.proxy.aws.AWSServiceSerdeModule;

public class Serializer {

public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
};
public static final String COMPRESSED = "__COMPRESSED__";
Expand Down Expand Up @@ -84,6 +83,16 @@ public class Serializer {
OBJECT_MAPPER.registerModule(new JavaTimeModule());
}

private final Boolean strictDeserialize;

public Serializer(Boolean strictDeserialize) {
this.strictDeserialize = strictDeserialize;
}

public Serializer() {
this.strictDeserialize = false;
}

public <T> String serialize(final T modelObject) throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsString(modelObject);
}
Expand All @@ -101,7 +110,11 @@ public <T> String compress(final String modelInput) throws IOException {
}

public <T> T deserialize(final String s, final TypeReference<T> reference) throws IOException {
return OBJECT_MAPPER.readValue(s, reference);
if (!strictDeserialize) {
return OBJECT_MAPPER.readValue(s, reference);
} else {
return deserializeStrict(s, reference);
}
}

public String decompress(final String s) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public HookLambdaWrapperOverride(final CredentialsProvider providerLoggingCreden
final MetricsPublisher providerMetricsPublisher,
final SchemaValidator validator,
final SdkHttpClient httpClient,
final Cipher cipher) {
final Cipher cipher,
final Boolean strictDeserialize) {
super(providerLoggingCredentialsProvider, providerEventsLogger, platformEventsLogger, providerMetricsPublisher, validator,
new Serializer(), httpClient, cipher);
new Serializer(strictDeserialize), httpClient, cipher);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
Expand All @@ -42,6 +43,7 @@
import software.amazon.cloudformation.loggers.LogPublisher;
import software.amazon.cloudformation.metrics.MetricsPublisher;
import software.amazon.cloudformation.proxy.Credentials;
import software.amazon.cloudformation.proxy.HandlerErrorCode;
import software.amazon.cloudformation.proxy.OperationStatus;
import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
Expand Down Expand Up @@ -83,11 +85,16 @@ public class HookLambdaWrapperTest {
private KMSCipher cipher;

private HookLambdaWrapperOverride wrapper;
private HookLambdaWrapperOverride wrapperStrictDeserialize;

@BeforeEach
public void initWrapper() {
wrapper = new HookLambdaWrapperOverride(providerLoggingCredentialsProvider, platformEventsLogger, providerEventsLogger,
providerMetricsPublisher, validator, httpClient, cipher);
providerMetricsPublisher, validator, httpClient, cipher, false);

wrapperStrictDeserialize = new HookLambdaWrapperOverride(providerLoggingCredentialsProvider, platformEventsLogger,
providerEventsLogger, providerMetricsPublisher, validator,
httpClient, cipher, true);
}

private static InputStream loadRequestStream(final String fileName) {
Expand Down Expand Up @@ -166,4 +173,161 @@ public void invokeHandler_CompleteSynchronously_returnsSuccess(final String requ
assertThat(wrapper.callbackContext).isNull();
}
}

@ParameterizedTest
@CsvSource({ "preCreate.request.with-resource-properties.json,CREATE_PRE_PROVISION" })
public void invokeHandler_WithResourceProperties_returnsSuccess(final String requestDataPath,
final String invocationPointString)
throws IOException {
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);

// if the handler responds Complete, this is treated as a successful synchronous
// completion
final ProgressEvent<TestModel,
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
wrapper.setInvokeHandlerResponse(pe);

lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));

wrapper.setTransformResponse(hookHandlerRequest);

try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
final Context context = getLambdaContext();

wrapper.handleRequest(in, out, context);

// verify initialiseRuntime was called and initialised dependencies
verifyInitialiseRuntime();

// verify output response
verifyHandlerResponse(out,
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());

// assert handler receives correct injections
assertThat(wrapper.awsClientProxy).isNotNull();
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
assertThat(wrapper.callbackContext).isNull();
}
}

@ParameterizedTest
@CsvSource({ "preCreate.request.with-resource-properties-and-extraneous-fields.json,CREATE_PRE_PROVISION" })
public void invokeHandler_WithResourcePropertiesAndExtraneousFields_returnsSuccess(final String requestDataPath,
final String invocationPointString)
throws IOException {
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);

// if the handler responds Complete, this is treated as a successful synchronous
// completion
final ProgressEvent<TestModel,
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
wrapper.setInvokeHandlerResponse(pe);

lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));

wrapper.setTransformResponse(hookHandlerRequest);

try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
final Context context = getLambdaContext();

wrapper.handleRequest(in, out, context);

// verify initialiseRuntime was called and initialised dependencies
verifyInitialiseRuntime();

// verify output response
verifyHandlerResponse(out,
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());

// assert handler receives correct injections
assertThat(wrapper.awsClientProxy).isNotNull();
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
assertThat(wrapper.callbackContext).isNull();
}
}

@ParameterizedTest
@CsvSource({ "preCreate.request.with-resource-properties.json,CREATE_PRE_PROVISION" })
public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSuccess(final String requestDataPath,
final String invocationPointString)
throws IOException {
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);

// if the handler responds Complete, this is treated as a successful synchronous
// completion
final ProgressEvent<TestModel,
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
wrapperStrictDeserialize.setInvokeHandlerResponse(pe);

lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));

wrapperStrictDeserialize.setTransformResponse(hookHandlerRequest);

try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
final Context context = getLambdaContext();

wrapperStrictDeserialize.handleRequest(in, out, context);

// verify initialiseRuntime was called and initialised dependencies
verifyInitialiseRuntime();

// verify output response
verifyHandlerResponse(out,
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());

// assert handler receives correct injections
assertThat(wrapperStrictDeserialize.awsClientProxy).isNotNull();
assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(hookHandlerRequest);
assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(invocationPoint);
assertThat(wrapperStrictDeserialize.callbackContext).isNull();
}
}

@ParameterizedTest
@CsvSource({ "preCreate.request.with-resource-properties-and-extraneous-fields.json" })
public void
invokeHandler_StrictDeserializer_WithResourcePropertiesAndExtraneousFields_returnsFailure(final String requestDataPath)
throws IOException {
// if the handler responds Complete, this is treated as a successful synchronous
// completion
final ProgressEvent<TestModel,
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
wrapperStrictDeserialize.setInvokeHandlerResponse(pe);

lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));

wrapperStrictDeserialize.setTransformResponse(hookHandlerRequest);

try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
final Context context = getLambdaContext();

wrapperStrictDeserialize.handleRequest(in, out, context);

// verify initialiseRuntime was called and initialised dependencies
verify(providerLoggingCredentialsProvider, times(0)).setCredentials(any(Credentials.class));
verify(providerMetricsPublisher, times(0)).refreshClient();

// verify output response
verifyHandlerResponse(out,
HookProgressEvent.<TestContext>builder().clientRequestToken(null).hookStatus(HookStatus.FAILED)
.errorCode(HandlerErrorCode.InternalFailure).callbackContext(null)
.message(expectedStringWhenStrictDeserializingWithExtraneousFields).build());

// assert handler receives correct injections
assertThat(wrapperStrictDeserialize.awsClientProxy).isNull();
assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(null);
assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(null);
assertThat(wrapperStrictDeserialize.callbackContext).isNull();
}
}

private final String expectedStringWhenStrictDeserializingWithExtraneousFields = "Unrecognized field \"targetName\" (class software.amazon.cloudformation.proxy.hook.HookInvocationRequest), not marked as ignorable (10 known properties: \"requestContext\", \"stackId\", \"clientRequestToken\", \"hookModel\", \"hookTypeName\", \"requestData\", \"actionInvocationPoint\", \"awsAccountId\", \"changeSetId\", \"hookTypeVersion\"])\n"
+ " at [Source: (String)\"{\n" + " \"clientRequestToken\": \"123456\",\n" + " \"awsAccountId\": \"123456789012\",\n"
+ " \"stackId\": \"arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968\",\n"
+ " \"changeSetId\": \"arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000\",\n"
+ " \"hookTypeName\": \"AWS::Test::TestModel\",\n" + " \"hookTypeVersion\": \"1.0\",\n" + " \"hookModel\": {\n"
+ " \"property1\": \"abc\",\n" + " \"property2\": 123\n" + " },\n"
+ " \"action\"[truncated 1935 chars]; line: 40, column: 20] (through reference chain: software.amazon.cloudformation.proxy.hook.HookInvocationRequest[\"targetName\"])";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"clientRequestToken": "123456",
"awsAccountId": "123456789012",
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968",
"changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000",
"hookTypeName": "AWS::Test::TestModel",
"hookTypeVersion": "1.0",
"hookModel": {
"property1": "abc",
"property2": 123
},
"actionInvocationPoint": "CREATE_PRE_PROVISION",
"requestData": {
"targetName": "AWS::Example::ResourceTarget",
"targetType": "RESOURCE",
"targetLogicalId": "myResource",
"targetModel": {
"resourceProperties": {
"BucketName": "someBucketName",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"BucketKeyEnabled": true,
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "someKMSMasterKeyID"
}
}
]
}
},
"previousResourceProperties": null
},
"callerCredentials": "callerCredentials",
"providerCredentials": "providerCredentials",
"providerLogGroupName": "providerLoggingGroupName",
"hookEncryptionKeyArn": "hookEncryptionKeyArn",
"hookEncryptionKeyRole": "hookEncryptionKeyRole"
},
"targetName": "STACK",
"template": "<Original json template as string>",
"previousTemplate": "<Original json template as string>",
"changedResources": [
{
"logicalId": "MyBucket",
"typeName": "AWS::S3::Bucket",
"lineNumber": 3,
"action": "CREATE",
"beforeContext": "<Resource Properties as json string>",
"afterContext": "<Resource Properties as json string>"
},
{
"logicalId": "MyBucketPolicy",
"typeName": "AWS::S3::BucketPolicy",
"lineNumber": 15,
"action": "CREATE",
"beforeContext": "<Resource Properties as json string>",
"afterContext": "<Resource Properties as json string>"
}
],
"requestContext": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"clientRequestToken": "123456",
"awsAccountId": "123456789012",
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968",
"changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000",
"hookTypeName": "AWS::Test::TestModel",
"hookTypeVersion": "1.0",
"hookModel": {
"property1": "abc",
"property2": 123
},
"actionInvocationPoint": "CREATE_PRE_PROVISION",
"requestData": {
"targetName": "AWS::Example::ResourceTarget",
"targetType": "RESOURCE",
"targetLogicalId": "myResource",
"targetModel": {
"resourceProperties": {
"BucketName": "someBucketName",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"BucketKeyEnabled": true,
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "someKMSMasterKeyID"
}
}
]
}
},
"previousResourceProperties": null
},
"callerCredentials": "callerCredentials",
"providerCredentials": "providerCredentials",
"providerLogGroupName": "providerLoggingGroupName",
"hookEncryptionKeyArn": "hookEncryptionKeyArn",
"hookEncryptionKeyRole": "hookEncryptionKeyRole"
},
"requestContext": {}
}