Skip to content

Commit

Permalink
token-aware-jsonify: Stringify resolved tokens (#406)
Browse files Browse the repository at this point in the history
`token-aware-jsonify`, which is currently used to 
serialize CloudWatch dashboard configuration into 
a JSON string is expected to return a stringified JSON object.
This means that any string within the object must be escaped (e.g. not
include "\n" or quotes).

The function currently stringifies the primary string but this still
leaves room for non-allowed characters in the resolved tokens. For example,
if a token resolves to `{ "Fn::Join": [ "", [ "Hello,\nWorld!" ] ] }` then
the deploy-time value will be "Hello,\nWorld!" which must be represented
in JSON as "Hello,\\nWorld!".

This change eagerly stringifies all string values in the resolved tokens.
Theoretically this might cause trouble in the case where token strings
have special characters AND not emitted, but this seems like a long shot
and we optimize for the common case.
  • Loading branch information
Elad Ben-Israel authored Jul 25, 2018
1 parent cda89b5 commit 264e6b5
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 9 deletions.
46 changes: 38 additions & 8 deletions packages/@aws-cdk/cdk/lib/cloudformation/token-aware-jsonify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import { FnSub } from './fn';
/**
* Jsonify a deep structure to a string while preserving tokens
*
* Sometimes we have JSON structures that contain CloudFormation
* intrinsics like { Ref } and { Fn::GetAtt }, but the model requires
* that we stringify the JSON structure and pass it into the parameter.
* Sometimes we have JSON structures that contain CloudFormation intrinsics like
* { Ref } and { Fn::GetAtt }, but the model requires that we stringify the JSON
* structure and pass it into the parameter.
*
* Doing this makes it so that CloudFormation does not resolve the intrinsics
* anymore, since it does not look into every string. To resolve this,
* we stringify into a string and put placeholders in wich we substitute
* with the resolved references using { Fn::Sub }.
* anymore, since it does not look into every string. To resolve this, we
* stringify into a string and put placeholders in wich we substitute with the
* resolved references using { Fn::Sub }.
*
* Since the result is expected to be a stringified JSON, we need to make sure
* any textual values resolved from tokens are also stringified, so we also
* stringify any string values in resolved tokens (for example, "\n" will be
* replaced by "\\n", quotes will be escaped, etc). This might not be needed (or
* even could be harmful) for certain tokens (e.g. Fn::GetAtt), but we prefer to
* make the common case fool-proof, and hope for the best.
*
* Will only work correctly for intrinsics that return a string value.
*/
Expand All @@ -30,12 +37,35 @@ export function tokenAwareJsonify(structure: any): any {
const tokenId: {[key: string]: string} = {};
const substitutionMap: {[key: string]: any} = {};

function stringifyStrings(x: any): any {
if (typeof(x) === 'string') {
const jsonS = JSON.stringify(x);
return jsonS.substr(1, jsonS.length - 2); // trim quotes
}

if (Array.isArray(x)) {
return x.map(stringifyStrings);
}

if (typeof(x) === 'object') {
const result: any = {};
for (const key of Object.keys(x)) {
result[key] = stringifyStrings(x[key]);
}

return result;
}

return x;
}

function rememberToken(x: Token) {
// Get a representation of the resolved Token that we can use as a hash key.
const reprKey = JSON.stringify(resolve(x));
const resolved = resolve(x);
const reprKey = JSON.stringify(resolved);
if (!(reprKey in tokenId)) {
tokenId[reprKey] = `ref${counter}`;
substitutionMap[tokenId[reprKey]] = x;
substitutionMap[tokenId[reprKey]] = stringifyStrings(resolved);
counter += 1;
}
return `<<<TOKEN:${tokenId[reprKey]}>>>`;
Expand Down
17 changes: 16 additions & 1 deletion packages/@aws-cdk/cdk/test/test.token-aware-jsonify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Test } from 'nodeunit';
import { AwsRegion, resolve, tokenAwareJsonify } from '../lib';
import { AwsRegion, FnConcat, resolve, tokenAwareJsonify } from '../lib';

export = {
'substitutes tokens'(test: Test) {
Expand Down Expand Up @@ -56,4 +56,19 @@ export = {

test.done();
},

'string values in resolved tokens should be represented as stringified strings'(test: Test) {
// WHEN
const result = tokenAwareJsonify({
test1: new FnConcat('Hello', 'This\nIs', 'Very "cool"'),
});

// THEN
test.deepEqual(resolve(result), { 'Fn::Sub':
[ '{"test1":"${ref0}"}',
{ ref0:
{ 'Fn::Join': [ '', [ 'Hello', 'This\\nIs', 'Very \\"cool\\"' ] ] } } ] });

test.done();
}
};

0 comments on commit 264e6b5

Please sign in to comment.