Skip to content

Commit

Permalink
feat(events): support embedded string variables (aws#13487)
Browse files Browse the repository at this point in the history
Event Bridge transformers have been updated to support embedded variable replacement within strings within objects. 

```
{
   data: "some string <myValue>"
}
```

Previously input transformers only supported string when they were the only value of an object, or just static strings.

```
// Before Event Bridges's change
{
   data: <myValue>, // OK
   data2: "some string", // OK
   data3: "some string <myValue>" // NOT OK 
}
```

The CDK solution was to assume that developers knew this restriction, wrap the string variable in special characters, and replace the double quotes plus special character set with nothing after token replacement.

This caused issues like aws#9191. Where string tokens (`EventField`) within a string would give a cryptic error during Cfn deployment due the resulting invalid object string generated (missing a closing double quote and leaving the special characters behind). 

### Solution:

Removed the special character sequence addition and stripping and instead only replace any instances of `"<myValue>"` that are added.

* Iterate over the known input transform keys to reduce possible unexpected impact and give developers a backdoor to change their keys in the worst case.
* Edge Case: `"<myValue>"` can appear with escaped quote sequences `"something \"quoted\"<myValue>"`. This is a valid string variable replacement case. Used a lookback regex (`(?<!\\\\)\"\<${key}\>\"`) to avoid the prefix escaped quote when replacing transform input keys with quote-less keys. 

### Tradeoffs

Removed the addition of special characters to find the keys in the final json string. Instead search for the specific pattern of transform input keys that should exist within the output and handle the edge case describe above.

This SHOULD cover all edge cases as it is not valid to have a trailing quote without an escape (`"<myValue>"" //not valid`) and it is not valid to have a prefix quote that is not escaped (`""<myValue>" // not valid`).

This was done to reduce the small change of overlapping with a developer's content, to be more targeted, and because the above should prove that the edge case is covered.


https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_InputTransformer.html

fixes aws#9191



----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
thantos authored and hollanddd committed Aug 26, 2021
1 parent 940e240 commit 67917f6
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 31 deletions.
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ onCommitRule.addTarget(new targets.SnsTopic(topic, {
}));
```

Or using an Object:

```ts
onCommitRule.addTarget(new targets.SnsTopic(topic, {
message: events.RuleTargetInput.fromObject(
{
DataType: `custom_${events.EventField.fromPath('$.detail-type')}`
}
)
}));
```

## Scheduling

You can configure a Rule to run on a schedule (cron or rate).
Expand Down
44 changes: 13 additions & 31 deletions packages/@aws-cdk/aws-events/lib/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ class FieldAwareEventInput extends RuleTargetInput {
return key;
}

const self = this;

class EventFieldReplacer extends DefaultTokenResolver {
constructor() {
super(new StringConcat());
Expand All @@ -167,7 +165,7 @@ class FieldAwareEventInput extends RuleTargetInput {
}
inputPathsMap[key] = t.path;

return self.keyPlaceholder(key);
return `<${key}>`;
}
}

Expand All @@ -188,35 +186,32 @@ class FieldAwareEventInput extends RuleTargetInput {
}));
}

if (Object.keys(inputPathsMap).length === 0) {
const keys = Object.keys(inputPathsMap);

if (keys.length === 0) {
// Nothing special, just return 'input'
return { input: resolved };
}

return {
inputTemplate: this.unquoteKeyPlaceholders(resolved),
inputTemplate: this.unquoteKeyPlaceholders(resolved, keys),
inputPathsMap,
};
}

/**
* Return a template placeholder for the given key
*
* In object scope we'll need to get rid of surrounding quotes later on, so
* return a bracing that's unlikely to occur naturally (like tokens).
*/
private keyPlaceholder(key: string) {
if (this.inputType !== InputType.Object) { return `<${key}>`; }
return UNLIKELY_OPENING_STRING + key + UNLIKELY_CLOSING_STRING;
}

/**
* Removing surrounding quotes from any object placeholders
* when key is the lone value.
*
* Those have been put there by JSON.stringify(), but we need to
* remove them.
*
* Do not remove quotes when the key is part of a larger string.
*
* Valid: { "data": "Some string with \"quotes\"<key>" } // key will be string
* Valid: { "data": <key> } // Key could be number, bool, obj, or string
*/
private unquoteKeyPlaceholders(sub: string) {
private unquoteKeyPlaceholders(sub: string, keys: string[]) {
if (this.inputType !== InputType.Object) { return sub; }

return Lazy.uncachedString({ produce: (ctx: IResolveContext) => Token.asString(deepUnquote(ctx.resolve(sub))) });
Expand All @@ -230,19 +225,13 @@ class FieldAwareEventInput extends RuleTargetInput {
}
return resolved;
} else if (typeof(resolved) === 'string') {
return resolved.replace(OPENING_STRING_REGEX, '<').replace(CLOSING_STRING_REGEX, '>');
return keys.reduce((r, key) => r.replace(new RegExp(`(?<!\\\\)\"\<${key}\>\"`, 'g'), `<${key}>`), resolved);
}
return resolved;
}
}
}

const UNLIKELY_OPENING_STRING = '<<${';
const UNLIKELY_CLOSING_STRING = '}>>';

const OPENING_STRING_REGEX = new RegExp(regexQuote('"' + UNLIKELY_OPENING_STRING), 'g');
const CLOSING_STRING_REGEX = new RegExp(regexQuote(UNLIKELY_CLOSING_STRING + '"'), 'g');

/**
* Represents a field in the event pattern
*/
Expand Down Expand Up @@ -339,10 +328,3 @@ function isEventField(x: any): x is EventField {
}

const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField');

/**
* Quote a string for use in a regex
*/
function regexQuote(s: string) {
return s.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
}
156 changes: 156 additions & 0 deletions packages/@aws-cdk/aws-events/test/test.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,162 @@ export = {
test.done();
},

'can use joined JSON containing refs in JSON object with tricky inputs'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `they said \"hello\"${EventField.fromPath('$')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"they said \\\"hello\\\"<f1>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and concat'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `more text ${EventField.fromPath('$')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"more text <f1>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and quotes'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `more text "${EventField.fromPath('$')}"`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"more text \\\"<f1>\\\"","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and multiple keys'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `${EventField.fromPath('$')}${EventField.fromPath('$.other')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"<f1><other>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use token'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down

0 comments on commit 67917f6

Please sign in to comment.