Skip to content

Commit

Permalink
Verify hook handler with extraneous fields (#431)
Browse files Browse the repository at this point in the history
* Verify that handler can handle input with extraneous fields
  • Loading branch information
brianlaoaws authored Dec 13, 2023
1 parent 53ab291 commit 5b354cf
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 5 deletions.
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": {}
}

0 comments on commit 5b354cf

Please sign in to comment.