diff --git a/internal/service/events/rule.go b/internal/service/events/rule.go index 6a6c4370662..3740591cb3a 100644 --- a/internal/service/events/rule.go +++ b/internal/service/events/rule.go @@ -1,7 +1,9 @@ package events import ( + "bytes" "context" + "encoding/json" "fmt" "log" "time" @@ -12,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" @@ -65,7 +66,7 @@ func ResourceRule() *schema.Resource { ValidateFunc: validateEventPatternValue(), AtLeastOneOf: []string{"schedule_expression", "event_pattern"}, StateFunc: func(v interface{}) string { - json, _ := structure.NormalizeJsonString(v.(string)) + json, _ := RuleEventPatternJSONDecoder(v.(string)) return json }, }, @@ -185,7 +186,7 @@ func resourceRuleRead(ctx context.Context, d *schema.ResourceData, meta interfac d.Set("description", output.Description) d.Set("event_bus_name", eventBusName) // Use event bus name from resource ID as API response may collapse any ARN. if output.EventPattern != nil { - pattern, err := structure.NormalizeJsonString(aws.StringValue(output.EventPattern)) + pattern, err := RuleEventPatternJSONDecoder(aws.StringValue(output.EventPattern)) if err != nil { return sdkdiag.AppendErrorf(diags, "event pattern contains an invalid JSON: %s", err) } @@ -295,6 +296,34 @@ func FindRuleByTwoPartKey(ctx context.Context, conn *eventbridge.EventBridge, ev return output, nil } +// RuleEventPatternJSONDecoder decodes unicode translation of <,>,& +func RuleEventPatternJSONDecoder(jsonString interface{}) (string, error) { + var j interface{} + + if jsonString == nil || jsonString.(string) == "" { + return "", nil + } + + s := jsonString.(string) + + err := json.Unmarshal([]byte(s), &j) + if err != nil { + return s, err + } + + b, err := json.Marshal(j) + if err != nil { + return "", err + } + + if bytes.Contains(b, []byte("\\u003c")) || bytes.Contains(b, []byte("\\u003e")) || bytes.Contains(b, []byte("\\u0026")) { + b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1) + b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1) + b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1) + } + return string(b[:]), nil +} + func expandPutRuleInput(d *schema.ResourceData, name string) *eventbridge.PutRuleInput { apiObject := &eventbridge.PutRuleInput{ Name: aws.String(name), @@ -309,7 +338,7 @@ func expandPutRuleInput(d *schema.ResourceData, name string) *eventbridge.PutRul } if v, ok := d.GetOk("event_pattern"); ok { - json, _ := structure.NormalizeJsonString(v) + json, _ := RuleEventPatternJSONDecoder(v.(string)) apiObject.EventPattern = aws.String(json) } @@ -332,7 +361,7 @@ func expandPutRuleInput(d *schema.ResourceData, name string) *eventbridge.PutRul func validateEventPatternValue() schema.SchemaValidateFunc { return func(v interface{}, k string) (ws []string, errors []error) { - json, err := structure.NormalizeJsonString(v) + json, err := RuleEventPatternJSONDecoder(v.(string)) if err != nil { errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %w", k, err)) diff --git a/internal/service/events/rule_test.go b/internal/service/events/rule_test.go index 6383dff823c..bd3486a5565 100644 --- a/internal/service/events/rule_test.go +++ b/internal/service/events/rule_test.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/eventbridge" + "github.com/google/go-cmp/cmp" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -29,6 +30,41 @@ func testAccErrorCheckSkip(t *testing.T) resource.ErrorCheckFunc { ) } +func TestRuleEventPatternJSONDecoder(t *testing.T) { + t.Parallel() + + type testCase struct { + input string + expected string + } + tests := map[string]testCase{ + "lessThanGreaterThan": { + input: `{"detail":{"count":[{"numeric":["\u003e",0,"\u003c",5]}]}}`, + expected: `{"detail":{"count":[{"numeric":[">",0,"<",5]}]}}`, + }, + "ampersand": { + input: `{"detail":{"count":[{"numeric":["\u0026",0,"\u0026",5]}]}}`, + expected: `{"detail":{"count":[{"numeric":["&",0,"&",5]}]}}`, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tfevents.RuleEventPatternJSONDecoder(test.input) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} + func TestAccEventsRule_basic(t *testing.T) { ctx := acctest.Context(t) var v1, v2, v3 eventbridge.DescribeRuleOutput @@ -256,6 +292,31 @@ func TestAccEventsRule_pattern(t *testing.T) { }) } +func TestAccEventsRule_patternJSONEncoder(t *testing.T) { + ctx := acctest.Context(t) + var v1 eventbridge.DescribeRuleOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudwatch_event_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, eventbridge.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRuleConfig_patternJSONEncoder(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRuleExists(ctx, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "schedule_expression", ""), + acctest.CheckResourceAttrEquivalentJSON(resourceName, "event_pattern", `{"detail":{"count":[{"numeric":[">",0,"<",5]}]}}`), + ), + }, + }, + }) +} + func TestAccEventsRule_scheduleAndPattern(t *testing.T) { ctx := acctest.Context(t) var v eventbridge.DescribeRuleOutput @@ -685,6 +746,15 @@ PATTERN `, rName, pattern) } +func testAccRuleConfig_patternJSONEncoder(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudwatch_event_rule" "test" { + name = %[1]q + event_pattern = jsonencode({ "detail" : { "count" : [{ "numeric" : [">", 0, "<", 5] }] } }) +} +`, rName) +} + func testAccRuleConfig_scheduleAndPattern(rName, pattern string) string { return fmt.Sprintf(` resource "aws_cloudwatch_event_rule" "test" {