diff --git a/.changelog/38309.txt b/.changelog/38309.txt new file mode 100644 index 00000000000..afe5184a38e --- /dev/null +++ b/.changelog/38309.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `rule_json` attribute to allow raw JSON for rules. +``` \ No newline at end of file diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index 884bd87f1ae..84550237da4 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -4,6 +4,9 @@ package wafv2 import ( + "encoding/json" + "fmt" + "reflect" "strings" "github.com/aws/aws-sdk-go-v2/aws" @@ -980,6 +983,22 @@ func expandHeaderMatchPattern(l []interface{}) *awstypes.HeaderMatchPattern { return f } +func expandWebACLRulesJSON(rawRules string) ([]awstypes.Rule, error) { + var rules []awstypes.Rule + + err := json.Unmarshal([]byte(rawRules), &rules) + if err != nil { + return nil, fmt.Errorf("decoding JSON: %s", err) + } + + for i, r := range rules { + if reflect.DeepEqual(r, awstypes.Rule{}) { + return nil, fmt.Errorf("invalid ACL Rule supplied at index (%d)", i) + } + } + return rules, nil +} + func expandWebACLRules(l []interface{}) []awstypes.Rule { if len(l) == 0 || l[0] == nil { return nil diff --git a/internal/service/wafv2/flex_test.go b/internal/service/wafv2/flex_test.go new file mode 100644 index 00000000000..860f1bcf58a --- /dev/null +++ b/internal/service/wafv2/flex_test.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package wafv2 + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awstypes "github.com/aws/aws-sdk-go-v2/service/wafv2/types" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_expandWebACLRulesJSON(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + rawRules string + want []awstypes.Rule + wantErr bool + }{ + "empty string": { + rawRules: "", + wantErr: true, + }, + "empty array": { + rawRules: "[]", + want: []awstypes.Rule{}, + }, + "single empty object": { + rawRules: "[{}]", + wantErr: true, + }, + "single null object": { + rawRules: "[null]", + wantErr: true, + }, + "valid object": { + rawRules: `[{"Action":{"Count":{}},"Name":"rule-1","Priority":1,"Statement":{"RateBasedStatement":{"AggregateKeyType":"IP","EvaluationWindowSec":600,"Limit":10000,"ScopeDownStatement":{"GeoMatchStatement":{"CountryCodes":["US","NL"]}}}},"VisibilityConfig":{"CloudwatchMetricsEnabled":false,"MetricName":"friendly-rule-metric-name","SampledRequestsEnabled":false}}]`, + want: []awstypes.Rule{ + { + Name: aws.String("rule-1"), + Priority: 1, + Action: &awstypes.RuleAction{ + Count: &awstypes.CountAction{}, + }, + Statement: &awstypes.Statement{ + RateBasedStatement: &awstypes.RateBasedStatement{ + Limit: aws.Int64(10000), + AggregateKeyType: awstypes.RateBasedStatementAggregateKeyType("IP"), + EvaluationWindowSec: 600, + ScopeDownStatement: &awstypes.Statement{ + GeoMatchStatement: &awstypes.GeoMatchStatement{ + CountryCodes: []awstypes.CountryCode{"US", "NL"}, + }, + }, + }, + }, + VisibilityConfig: &awstypes.VisibilityConfig{ + CloudWatchMetricsEnabled: false, + MetricName: aws.String("friendly-rule-metric-name"), + SampledRequestsEnabled: false, + }, + }, + }, + }, + "valid and empty object": { + rawRules: `[{"Action":{"Count":{}},"Name":"rule-1","Priority":1,"Statement":{"RateBasedStatement":{"AggregateKeyType":"IP","EvaluationWindowSec":600,"Limit":10000,"ScopeDownStatement":{"GeoMatchStatement":{"CountryCodes":["US","NL"]}}}},"VisibilityConfig":{"CloudwatchMetricsEnabled":false,"MetricName":"friendly-rule-metric-name","SampledRequestsEnabled":false}},{}]`, + wantErr: true, + }, + } + + ignoreExportedOpts := cmpopts.IgnoreUnexported( + awstypes.Rule{}, + awstypes.RuleAction{}, + awstypes.CountAction{}, + awstypes.Statement{}, + awstypes.RateBasedStatement{}, + awstypes.GeoMatchStatement{}, + awstypes.VisibilityConfig{}, + ) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := expandWebACLRulesJSON(tc.rawRules) + if (err != nil) != tc.wantErr { + t.Errorf("expandWebACLRulesJSON() error = %v, wantErr %v", err, tc.wantErr) + return + } + if diff := cmp.Diff(got, tc.want, ignoreExportedOpts); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/internal/service/wafv2/web_acl.go b/internal/service/wafv2/web_acl.go index a6e0ce0d343..b02756e090c 100644 --- a/internal/service/wafv2/web_acl.go +++ b/internal/service/wafv2/web_acl.go @@ -17,6 +17,7 @@ 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/enum" @@ -101,9 +102,21 @@ func resourceWebACL() *schema.Resource { validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z_-]+$`), "must contain only alphanumeric hyphen and underscore characters"), ), }, + "rule_json": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{names.AttrRule}, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, names.AttrRule: { - Type: schema.TypeSet, - Optional: true, + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"rule_json"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ names.AttrAction: { @@ -179,18 +192,30 @@ func resourceWebACLCreate(ctx context.Context, d *schema.ResourceData, meta inte conn := meta.(*conns.AWSClient).WAFV2Client(ctx) name := d.Get(names.AttrName).(string) + input := &wafv2.CreateWebACLInput{ AssociationConfig: expandAssociationConfig(d.Get("association_config").([]interface{})), CaptchaConfig: expandCaptchaConfig(d.Get("captcha_config").([]interface{})), ChallengeConfig: expandChallengeConfig(d.Get("challenge_config").([]interface{})), DefaultAction: expandDefaultAction(d.Get(names.AttrDefaultAction).([]interface{})), Name: aws.String(name), - Rules: expandWebACLRules(d.Get(names.AttrRule).(*schema.Set).List()), Scope: awstypes.Scope(d.Get(names.AttrScope).(string)), Tags: getTagsIn(ctx), VisibilityConfig: expandVisibilityConfig(d.Get("visibility_config").([]interface{})), } + if v, ok := d.GetOk(names.AttrRule); ok { + input.Rules = expandWebACLRules(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("rule_json"); ok { + rules, err := expandWebACLRulesJSON(v.(string)) + if err != nil { + return sdkdiag.AppendErrorf(diags, "setting rule: %s", err) + } + input.Rules = rules + } + if v, ok := d.GetOk("custom_response_body"); ok && v.(*schema.Set).Len() > 0 { input.CustomResponseBodies = expandCustomResponseBodies(v.(*schema.Set).List()) } @@ -259,10 +284,16 @@ func resourceWebACLRead(ctx context.Context, d *schema.ResourceData, meta interf d.Set(names.AttrDescription, webACL.Description) d.Set("lock_token", output.LockToken) d.Set(names.AttrName, webACL.Name) - rules := filterWebACLRules(webACL.Rules, expandWebACLRules(d.Get(names.AttrRule).(*schema.Set).List())) - if err := d.Set(names.AttrRule, flattenWebACLRules(rules)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting rule: %s", err) + + if _, ok := d.GetOk(names.AttrRule); ok { + rules := filterWebACLRules(webACL.Rules, expandWebACLRules(d.Get(names.AttrRule).(*schema.Set).List())) + if err := d.Set(names.AttrRule, flattenWebACLRules(rules)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting rule: %s", err) + } } + + d.Set("rule_json", d.Get("rule_json")) + d.Set("token_domains", aws.StringSlice(webACL.TokenDomains)) if err := d.Set("visibility_config", flattenVisibilityConfig(webACL.VisibilityConfig)); err != nil { return sdkdiag.AppendErrorf(diags, "setting visibility_config: %s", err) @@ -281,7 +312,9 @@ func resourceWebACLUpdate(ctx context.Context, d *schema.ResourceData, meta inte aclLockToken := d.Get("lock_token").(string) // Find the AWS managed ShieldMitigationRuleGroup group rule if existent and add it into the set of rules to update // so that the provider will not remove the Shield rule when changes are applied to the WebACL. - rules := expandWebACLRules(d.Get(names.AttrRule).(*schema.Set).List()) + var rules []awstypes.Rule + + rules = expandWebACLRules(d.Get(names.AttrRule).(*schema.Set).List()) if sr := findShieldRule(rules); len(sr) == 0 { output, err := findWebACLByThreePartKey(ctx, conn, d.Id(), aclName, aclScope) @@ -292,6 +325,23 @@ func resourceWebACLUpdate(ctx context.Context, d *schema.ResourceData, meta inte rules = append(rules, findShieldRule(output.WebACL.Rules)...) } + if d.HasChange("rule_json") { + r, err := expandWebACLRulesJSON(d.Get("rule_json").(string)) + if err != nil { + return sdkdiag.AppendErrorf(diags, "expanding WAFv2 WebACL JSON rule (%s): %s", d.Id(), err) + } + if sr := findShieldRule(rules); len(sr) == 0 { + output, err := findWebACLByThreePartKey(ctx, conn, d.Id(), aclName, aclScope) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading WAFv2 WebACL (%s): %s", d.Id(), err) + } + + r = append(r, findShieldRule(output.WebACL.Rules)...) + } + rules = r + } + input := &wafv2.UpdateWebACLInput{ AssociationConfig: expandAssociationConfig(d.Get("association_config").([]interface{})), CaptchaConfig: expandCaptchaConfig(d.Get("captcha_config").([]interface{})), diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index e6b920a1a15..9f8e7843361 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -215,10 +215,11 @@ func TestAccWAFV2WebACL_Update_rule(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -422,10 +423,11 @@ func TestAccWAFV2WebACL_Update_ruleProperties(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -635,10 +637,11 @@ func TestAccWAFV2WebACL_ManagedRuleGroup_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -708,10 +711,11 @@ func TestAccWAFV2WebACL_ManagedRuleGroup_ManagedRuleGroupConfig(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -815,10 +819,11 @@ func TestAccWAFV2WebACL_ManagedRuleGroup_ManagedRuleGroupConfig_ACFPRuleSet(t *t ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -902,10 +907,11 @@ func TestAccWAFV2WebACL_ManagedRuleGroup_ManagedRuleGroupConfig_ATPRuleSet(t *te ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1008,10 +1014,11 @@ func TestAccWAFV2WebACL_ManagedRuleGroup_specifyVersion(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1114,10 +1121,11 @@ func TestAccWAFV2WebACL_RateBased_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1176,10 +1184,11 @@ func TestAccWAFV2WebACL_ByteMatchStatement_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1224,10 +1233,11 @@ func TestAccWAFV2WebACL_ByteMatchStatement_ja3fingerprint(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1284,10 +1294,11 @@ func TestAccWAFV2WebACL_ByteMatchStatement_jsonBody(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1332,10 +1343,11 @@ func TestAccWAFV2WebACL_ByteMatchStatement_body(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1380,10 +1392,11 @@ func TestAccWAFV2WebACL_ByteMatchStatement_headerOrder(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1473,10 +1486,11 @@ func TestAccWAFV2WebACL_GeoMatch_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1539,10 +1553,11 @@ func TestAccWAFV2WebACL_GeoMatch_forwardedIP(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1591,10 +1606,11 @@ func TestAccWAFV2WebACL_LabelMatchStatement(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1639,10 +1655,11 @@ func TestAccWAFV2WebACL_RuleLabels(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1686,10 +1703,11 @@ func TestAccWAFV2WebACL_IPSetReference_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -1791,10 +1809,11 @@ func TestAccWAFV2WebACL_IPSetReference_forwardedIP(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2063,10 +2082,11 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2135,10 +2155,11 @@ func TestAccWAFV2WebACL_RateBased_forwardedIP(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2201,10 +2222,11 @@ func TestAccWAFV2WebACL_RuleGroupReference_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2456,10 +2478,11 @@ func TestAccWAFV2WebACL_Custom_requestHandling(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, { Config: testAccWebACLConfig_customRequestHandlingCount(webACLName, "x-hdr1", "x-hdr2"), @@ -2558,6 +2581,7 @@ func TestAccWAFV2WebACL_Custom_requestHandling(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "visibility_config.0.metric_name", "friendly-metric-name"), resource.TestCheckResourceAttr(resourceName, "visibility_config.0.sampled_requests_enabled", acctest.CtFalse), ), + ImportStateVerifyIgnore: []string{names.AttrRule}, }, }, }) @@ -2674,10 +2698,11 @@ func TestAccWAFV2WebACL_Custom_response(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2768,10 +2793,11 @@ func TestAccWAFV2WebACL_RateBased_maxNested(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -2813,10 +2839,11 @@ func TestAccWAFV2WebACL_Operators_maxNested(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -3010,10 +3037,42 @@ func TestAccWAFV2WebACL_CloudFrontScope(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerifyIgnore: []string{names.AttrRule}, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + }, + }, + }) +} + +func TestAccWAFV2WebACL_ruleJSON(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.WebACL + webACLName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckScopeRegional(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLConfig_JSONrule(webACLName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "rule_json"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"rule_json"}, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), }, }, }) @@ -6055,3 +6114,49 @@ resource "aws_wafv2_web_acl" "test" { } `, rName) } + +func testAccWebACLConfig_JSONrule(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } + + rule_json = jsonencode([{ + Name = "rule-1", + Priority = 1, + Action = { + Count = {} + }, + Statement = { + RateBasedStatement = { + Limit = 10000, + AggregateKeyType = "IP", + EvaluationWindowSec = 600, + ScopeDownStatement = { + GeoMatchStatement = { + CountryCodes = ["US", "NL"] + }, + }, + }, + }, + + VisibilityConfig = { + CloudwatchMetricsEnabled = false, + MetricName = "friendly-rule-metric-name", + SampledRequestsEnabled = false, + }, + }]) +} +`, rName) +} diff --git a/tools/tfsdk2fw/go.sum b/tools/tfsdk2fw/go.sum index 49a37b67777..3bc891e9c3f 100644 --- a/tools/tfsdk2fw/go.sum +++ b/tools/tfsdk2fw/go.sum @@ -785,9 +785,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= diff --git a/website/docs/r/wafv2_web_acl.html.markdown b/website/docs/r/wafv2_web_acl.html.markdown index 9f784a4f0e0..924c15446a8 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -465,6 +465,7 @@ This resource supports the following arguments: * `description` - (Optional) Friendly description of the WebACL. * `name` - (Required, Forces new resource) Friendly name of the WebACL. * `rule` - (Optional) Rule blocks used to identify the web requests that you want to `allow`, `block`, or `count`. See [`rule`](#rule-block) below for details. +* `rule_json` (Optional) Raw JSON string to allow more than three nested statements. Conflicts with `rule` attribute. This is for advanced use cases where more than 3 levels of nested statements are required. **There is no drift detection at this time**. If you use this attribute instead of `rule`, you will be foregoing drift detection. See the AWS [documentation](https://docs.aws.amazon.com/waf/latest/APIReference/API_CreateWebACL.html) for the JSON structure. * `scope` - (Required, Forces new resource) Specifies whether this is for an AWS CloudFront distribution or for a regional application. Valid values are `CLOUDFRONT` or `REGIONAL`. To work with CloudFront, you must also specify the region `us-east-1` (N. Virginia) on the AWS provider. * `tags` - (Optional) Map of key-value pairs to associate with the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `token_domains` - (Optional) Specifies the domains that AWS WAF should accept in a web request token. This enables the use of tokens across multiple protected websites. When AWS WAF provides a token, it uses the domain of the AWS resource that the web ACL is protecting. If you don't specify a list of token domains, AWS WAF accepts tokens only for the domain of the protected resource. With a token domain list, AWS WAF accepts the resource's host domain plus all domains in the token domain list, including their prefixed subdomains.