Skip to content

Commit 5b354cf

Browse files
authored
Verify hook handler with extraneous fields (#431)
* Verify that handler can handle input with extraneous fields
1 parent 53ab291 commit 5b354cf

File tree

5 files changed

+286
-5
lines changed

5 files changed

+286
-5
lines changed

src/main/java/software/amazon/cloudformation/resource/Serializer.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import software.amazon.cloudformation.proxy.aws.AWSServiceSerdeModule;
3737

3838
public class Serializer {
39-
4039
public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
4140
};
4241
public static final String COMPRESSED = "__COMPRESSED__";
@@ -84,6 +83,16 @@ public class Serializer {
8483
OBJECT_MAPPER.registerModule(new JavaTimeModule());
8584
}
8685

86+
private final Boolean strictDeserialize;
87+
88+
public Serializer(Boolean strictDeserialize) {
89+
this.strictDeserialize = strictDeserialize;
90+
}
91+
92+
public Serializer() {
93+
this.strictDeserialize = false;
94+
}
95+
8796
public <T> String serialize(final T modelObject) throws JsonProcessingException {
8897
return OBJECT_MAPPER.writeValueAsString(modelObject);
8998
}
@@ -101,7 +110,11 @@ public <T> String compress(final String modelInput) throws IOException {
101110
}
102111

103112
public <T> T deserialize(final String s, final TypeReference<T> reference) throws IOException {
104-
return OBJECT_MAPPER.readValue(s, reference);
113+
if (!strictDeserialize) {
114+
return OBJECT_MAPPER.readValue(s, reference);
115+
} else {
116+
return deserializeStrict(s, reference);
117+
}
105118
}
106119

107120
public String decompress(final String s) throws IOException {

src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ public HookLambdaWrapperOverride(final CredentialsProvider providerLoggingCreden
5353
final MetricsPublisher providerMetricsPublisher,
5454
final SchemaValidator validator,
5555
final SdkHttpClient httpClient,
56-
final Cipher cipher) {
56+
final Cipher cipher,
57+
final Boolean strictDeserialize) {
5758
super(providerLoggingCredentialsProvider, providerEventsLogger, platformEventsLogger, providerMetricsPublisher, validator,
58-
new Serializer(), httpClient, cipher);
59+
new Serializer(strictDeserialize), httpClient, cipher);
5960
}
6061

6162
@Override

src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java

+165-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.mockito.ArgumentMatchers.any;
1919
import static org.mockito.Mockito.lenient;
2020
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.times;
2122
import static org.mockito.Mockito.verify;
2223
import com.amazonaws.services.lambda.runtime.Context;
2324
import com.amazonaws.services.lambda.runtime.LambdaLogger;
@@ -42,6 +43,7 @@
4243
import software.amazon.cloudformation.loggers.LogPublisher;
4344
import software.amazon.cloudformation.metrics.MetricsPublisher;
4445
import software.amazon.cloudformation.proxy.Credentials;
46+
import software.amazon.cloudformation.proxy.HandlerErrorCode;
4547
import software.amazon.cloudformation.proxy.OperationStatus;
4648
import software.amazon.cloudformation.proxy.ProgressEvent;
4749
import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
@@ -83,11 +85,16 @@ public class HookLambdaWrapperTest {
8385
private KMSCipher cipher;
8486

8587
private HookLambdaWrapperOverride wrapper;
88+
private HookLambdaWrapperOverride wrapperStrictDeserialize;
8689

8790
@BeforeEach
8891
public void initWrapper() {
8992
wrapper = new HookLambdaWrapperOverride(providerLoggingCredentialsProvider, platformEventsLogger, providerEventsLogger,
90-
providerMetricsPublisher, validator, httpClient, cipher);
93+
providerMetricsPublisher, validator, httpClient, cipher, false);
94+
95+
wrapperStrictDeserialize = new HookLambdaWrapperOverride(providerLoggingCredentialsProvider, platformEventsLogger,
96+
providerEventsLogger, providerMetricsPublisher, validator,
97+
httpClient, cipher, true);
9198
}
9299

93100
private static InputStream loadRequestStream(final String fileName) {
@@ -166,4 +173,161 @@ public void invokeHandler_CompleteSynchronously_returnsSuccess(final String requ
166173
assertThat(wrapper.callbackContext).isNull();
167174
}
168175
}
176+
177+
@ParameterizedTest
178+
@CsvSource({ "preCreate.request.with-resource-properties.json,CREATE_PRE_PROVISION" })
179+
public void invokeHandler_WithResourceProperties_returnsSuccess(final String requestDataPath,
180+
final String invocationPointString)
181+
throws IOException {
182+
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);
183+
184+
// if the handler responds Complete, this is treated as a successful synchronous
185+
// completion
186+
final ProgressEvent<TestModel,
187+
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
188+
wrapper.setInvokeHandlerResponse(pe);
189+
190+
lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));
191+
192+
wrapper.setTransformResponse(hookHandlerRequest);
193+
194+
try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
195+
final Context context = getLambdaContext();
196+
197+
wrapper.handleRequest(in, out, context);
198+
199+
// verify initialiseRuntime was called and initialised dependencies
200+
verifyInitialiseRuntime();
201+
202+
// verify output response
203+
verifyHandlerResponse(out,
204+
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());
205+
206+
// assert handler receives correct injections
207+
assertThat(wrapper.awsClientProxy).isNotNull();
208+
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
209+
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
210+
assertThat(wrapper.callbackContext).isNull();
211+
}
212+
}
213+
214+
@ParameterizedTest
215+
@CsvSource({ "preCreate.request.with-resource-properties-and-extraneous-fields.json,CREATE_PRE_PROVISION" })
216+
public void invokeHandler_WithResourcePropertiesAndExtraneousFields_returnsSuccess(final String requestDataPath,
217+
final String invocationPointString)
218+
throws IOException {
219+
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);
220+
221+
// if the handler responds Complete, this is treated as a successful synchronous
222+
// completion
223+
final ProgressEvent<TestModel,
224+
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
225+
wrapper.setInvokeHandlerResponse(pe);
226+
227+
lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));
228+
229+
wrapper.setTransformResponse(hookHandlerRequest);
230+
231+
try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
232+
final Context context = getLambdaContext();
233+
234+
wrapper.handleRequest(in, out, context);
235+
236+
// verify initialiseRuntime was called and initialised dependencies
237+
verifyInitialiseRuntime();
238+
239+
// verify output response
240+
verifyHandlerResponse(out,
241+
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());
242+
243+
// assert handler receives correct injections
244+
assertThat(wrapper.awsClientProxy).isNotNull();
245+
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
246+
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
247+
assertThat(wrapper.callbackContext).isNull();
248+
}
249+
}
250+
251+
@ParameterizedTest
252+
@CsvSource({ "preCreate.request.with-resource-properties.json,CREATE_PRE_PROVISION" })
253+
public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSuccess(final String requestDataPath,
254+
final String invocationPointString)
255+
throws IOException {
256+
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);
257+
258+
// if the handler responds Complete, this is treated as a successful synchronous
259+
// completion
260+
final ProgressEvent<TestModel,
261+
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
262+
wrapperStrictDeserialize.setInvokeHandlerResponse(pe);
263+
264+
lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));
265+
266+
wrapperStrictDeserialize.setTransformResponse(hookHandlerRequest);
267+
268+
try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
269+
final Context context = getLambdaContext();
270+
271+
wrapperStrictDeserialize.handleRequest(in, out, context);
272+
273+
// verify initialiseRuntime was called and initialised dependencies
274+
verifyInitialiseRuntime();
275+
276+
// verify output response
277+
verifyHandlerResponse(out,
278+
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());
279+
280+
// assert handler receives correct injections
281+
assertThat(wrapperStrictDeserialize.awsClientProxy).isNotNull();
282+
assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(hookHandlerRequest);
283+
assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(invocationPoint);
284+
assertThat(wrapperStrictDeserialize.callbackContext).isNull();
285+
}
286+
}
287+
288+
@ParameterizedTest
289+
@CsvSource({ "preCreate.request.with-resource-properties-and-extraneous-fields.json" })
290+
public void
291+
invokeHandler_StrictDeserializer_WithResourcePropertiesAndExtraneousFields_returnsFailure(final String requestDataPath)
292+
throws IOException {
293+
// if the handler responds Complete, this is treated as a successful synchronous
294+
// completion
295+
final ProgressEvent<TestModel,
296+
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
297+
wrapperStrictDeserialize.setInvokeHandlerResponse(pe);
298+
299+
lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));
300+
301+
wrapperStrictDeserialize.setTransformResponse(hookHandlerRequest);
302+
303+
try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
304+
final Context context = getLambdaContext();
305+
306+
wrapperStrictDeserialize.handleRequest(in, out, context);
307+
308+
// verify initialiseRuntime was called and initialised dependencies
309+
verify(providerLoggingCredentialsProvider, times(0)).setCredentials(any(Credentials.class));
310+
verify(providerMetricsPublisher, times(0)).refreshClient();
311+
312+
// verify output response
313+
verifyHandlerResponse(out,
314+
HookProgressEvent.<TestContext>builder().clientRequestToken(null).hookStatus(HookStatus.FAILED)
315+
.errorCode(HandlerErrorCode.InternalFailure).callbackContext(null)
316+
.message(expectedStringWhenStrictDeserializingWithExtraneousFields).build());
317+
318+
// assert handler receives correct injections
319+
assertThat(wrapperStrictDeserialize.awsClientProxy).isNull();
320+
assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(null);
321+
assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(null);
322+
assertThat(wrapperStrictDeserialize.callbackContext).isNull();
323+
}
324+
}
325+
326+
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"
327+
+ " at [Source: (String)\"{\n" + " \"clientRequestToken\": \"123456\",\n" + " \"awsAccountId\": \"123456789012\",\n"
328+
+ " \"stackId\": \"arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968\",\n"
329+
+ " \"changeSetId\": \"arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000\",\n"
330+
+ " \"hookTypeName\": \"AWS::Test::TestModel\",\n" + " \"hookTypeVersion\": \"1.0\",\n" + " \"hookModel\": {\n"
331+
+ " \"property1\": \"abc\",\n" + " \"property2\": 123\n" + " },\n"
332+
+ " \"action\"[truncated 1935 chars]; line: 40, column: 20] (through reference chain: software.amazon.cloudformation.proxy.hook.HookInvocationRequest[\"targetName\"])";
169333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"clientRequestToken": "123456",
3+
"awsAccountId": "123456789012",
4+
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968",
5+
"changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000",
6+
"hookTypeName": "AWS::Test::TestModel",
7+
"hookTypeVersion": "1.0",
8+
"hookModel": {
9+
"property1": "abc",
10+
"property2": 123
11+
},
12+
"actionInvocationPoint": "CREATE_PRE_PROVISION",
13+
"requestData": {
14+
"targetName": "AWS::Example::ResourceTarget",
15+
"targetType": "RESOURCE",
16+
"targetLogicalId": "myResource",
17+
"targetModel": {
18+
"resourceProperties": {
19+
"BucketName": "someBucketName",
20+
"BucketEncryption": {
21+
"ServerSideEncryptionConfiguration": [
22+
{
23+
"BucketKeyEnabled": true,
24+
"ServerSideEncryptionByDefault": {
25+
"SSEAlgorithm": "aws:kms",
26+
"KMSMasterKeyID": "someKMSMasterKeyID"
27+
}
28+
}
29+
]
30+
}
31+
},
32+
"previousResourceProperties": null
33+
},
34+
"callerCredentials": "callerCredentials",
35+
"providerCredentials": "providerCredentials",
36+
"providerLogGroupName": "providerLoggingGroupName",
37+
"hookEncryptionKeyArn": "hookEncryptionKeyArn",
38+
"hookEncryptionKeyRole": "hookEncryptionKeyRole"
39+
},
40+
"targetName": "STACK",
41+
"template": "<Original json template as string>",
42+
"previousTemplate": "<Original json template as string>",
43+
"changedResources": [
44+
{
45+
"logicalId": "MyBucket",
46+
"typeName": "AWS::S3::Bucket",
47+
"lineNumber": 3,
48+
"action": "CREATE",
49+
"beforeContext": "<Resource Properties as json string>",
50+
"afterContext": "<Resource Properties as json string>"
51+
},
52+
{
53+
"logicalId": "MyBucketPolicy",
54+
"typeName": "AWS::S3::BucketPolicy",
55+
"lineNumber": 15,
56+
"action": "CREATE",
57+
"beforeContext": "<Resource Properties as json string>",
58+
"afterContext": "<Resource Properties as json string>"
59+
}
60+
],
61+
"requestContext": {}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"clientRequestToken": "123456",
3+
"awsAccountId": "123456789012",
4+
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968",
5+
"changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000",
6+
"hookTypeName": "AWS::Test::TestModel",
7+
"hookTypeVersion": "1.0",
8+
"hookModel": {
9+
"property1": "abc",
10+
"property2": 123
11+
},
12+
"actionInvocationPoint": "CREATE_PRE_PROVISION",
13+
"requestData": {
14+
"targetName": "AWS::Example::ResourceTarget",
15+
"targetType": "RESOURCE",
16+
"targetLogicalId": "myResource",
17+
"targetModel": {
18+
"resourceProperties": {
19+
"BucketName": "someBucketName",
20+
"BucketEncryption": {
21+
"ServerSideEncryptionConfiguration": [
22+
{
23+
"BucketKeyEnabled": true,
24+
"ServerSideEncryptionByDefault": {
25+
"SSEAlgorithm": "aws:kms",
26+
"KMSMasterKeyID": "someKMSMasterKeyID"
27+
}
28+
}
29+
]
30+
}
31+
},
32+
"previousResourceProperties": null
33+
},
34+
"callerCredentials": "callerCredentials",
35+
"providerCredentials": "providerCredentials",
36+
"providerLogGroupName": "providerLoggingGroupName",
37+
"hookEncryptionKeyArn": "hookEncryptionKeyArn",
38+
"hookEncryptionKeyRole": "hookEncryptionKeyRole"
39+
},
40+
"requestContext": {}
41+
}

0 commit comments

Comments
 (0)