From 913dc98f21905429f7e899610593e60e11e869ee Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 20 Dec 2021 15:27:43 +0100 Subject: [PATCH] feat(custom-resources): support NoEcho for sensitive data --- packages/@aws-cdk/custom-resources/README.md | 1 + .../provider-framework/runtime/framework.ts | 6 +- .../lib/provider-framework/types.d.ts | 9 +++ .../test/provider-framework/runtime.test.ts | 55 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 2c15ba9141e17..890255ee7bb12 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -143,6 +143,7 @@ The return value from `onEvent` must be a JSON object with the following fields: |-----|----|--------|----------- |`PhysicalResourceId`|String|No|The allocated/assigned physical ID of the resource. If omitted for `Create` events, the event's `RequestId` will be used. For `Update`, the current physical ID will be used. If a different value is returned, CloudFormation will follow with a subsequent `Delete` for the previous ID (resource replacement). For `Delete`, it will always return the current physical resource ID, and if the user returns a different one, an error will occur. |`Data`|JSON|No|Resource attributes, which can later be retrieved through `Fn::GetAtt` on the custom resource object. +|`NoEcho`|Boolean|No|Whether to mask the output of the custom resource when retrieved by using the `Fn::GetAtt` function. |*any*|*any*|No|Any other field included in the response will be passed through to `isComplete`. This can sometimes be useful to pass state between the handlers. [Custom Resource Provider Request]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html#crpg-ref-request-fields diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts index eda70ea7efb4f..181968611d354 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts @@ -39,7 +39,7 @@ async function onEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceEvent) // determine if this is an async provider based on whether we have an isComplete handler defined. // if it is not defined, then we are basically ready to return a positive response. if (!process.env[consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV]) { - return cfnResponse.submitResponse('SUCCESS', resourceEvent); + return cfnResponse.submitResponse('SUCCESS', resourceEvent, { noEcho: resourceEvent.NoEcho }); } // ok, we are not complete, so kick off the waiter workflow @@ -62,7 +62,7 @@ async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) { const isCompleteResult = await invokeUserFunction(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, event) as IsCompleteResponse; log('user isComplete returned:', isCompleteResult); - // if we are not complete, reeturn false, and don't send a response back. + // if we are not complete, return false, and don't send a response back. if (!isCompleteResult.IsComplete) { if (isCompleteResult.Data && Object.keys(isCompleteResult.Data).length > 0) { throw new Error('"Data" is not allowed if "IsComplete" is "False"'); @@ -79,7 +79,7 @@ async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) { }, }; - await cfnResponse.submitResponse('SUCCESS', response); + await cfnResponse.submitResponse('SUCCESS', response, { noEcho: event.NoEcho }); } // invoked when completion retries are exhaused. diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts index 33a125a971cca..9a9536eac078b 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts @@ -80,6 +80,15 @@ interface OnEventResponse { * Custom fields returned from OnEvent will be passed to IsComplete. */ readonly [key: string]: any; + + /** + * Whether to mask the output of the custom resource when retrieved + * by using the `Fn::GetAtt` function. If set to `true`, all returned + * values are masked with asterisks (*****). + * + * @default false + */ + readonly NoEcho?: boolean; } /** diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts index 45cc13a460959..d2af0a4fafd2d 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts @@ -230,6 +230,61 @@ test('if there is no user-defined "isComplete", the waiter will not be triggered expectCloudFormationSuccess({ PhysicalResourceId: MOCK_PHYSICAL_ID }); }); +describe('NoEcho', () => { + test('with onEvent', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expectCloudFormationSuccess({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + }); + + test('with isComplete', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + mocks.isCompleteImplMock = async () => ({ + Data: { + Also: 'Confidential', + }, + IsComplete: true, + }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expectCloudFormationSuccess({ + Data: { + Very: 'Sensitive', + Also: 'Confidential', + }, + NoEcho: true, + }); + }); +}); + test('fails if user handler returns a non-object response', async () => { // GIVEN mocks.stringifyPayload = false;