diff --git a/docs/contributing/maintaining.md b/docs/contributing/maintaining.md index fe790193742..f0f27bbc37a 100644 --- a/docs/contributing/maintaining.md +++ b/docs/contributing/maintaining.md @@ -412,6 +412,8 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi | `TF_ACC` | Enables Go tests containing `resource.Test()` and `resource.ParallelTest()`. | | `TF_ACC_ASSUME_ROLE_ARN` | Amazon Resource Name of existing IAM Role to use for limited permissions acceptance testing. | | `TF_TEST_CLOUDFRONT_RETAIN` | Flag to disable but dangle CloudFront Distributions during testing to reduce feedback time (must be manually destroyed afterwards) | +| `WAF_SUBSCRIBED_RULE_GROUP_NAME` | Subscribed rule group name for WAF testing. Should be set to the name of an existing subscribed rule group within the account. | +| `WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME` | The name of the metric measured by the subscribed rule group used for WAF testing. Required as the rule group name is not a unique identifier. | ## Label Dictionary diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 19a22192286..fe0614d9f18 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -887,15 +887,17 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_transfer_server": transfer.DataSourceServer(), - "aws_waf_ipset": waf.DataSourceIPSet(), - "aws_waf_rule": waf.DataSourceRule(), - "aws_waf_rate_based_rule": waf.DataSourceRateBasedRule(), - "aws_waf_web_acl": waf.DataSourceWebACL(), - - "aws_wafregional_ipset": wafregional.DataSourceIPSet(), - "aws_wafregional_rule": wafregional.DataSourceRule(), - "aws_wafregional_rate_based_rule": wafregional.DataSourceRateBasedRule(), - "aws_wafregional_web_acl": wafregional.DataSourceWebACL(), + "aws_waf_ipset": waf.DataSourceIPSet(), + "aws_waf_rule": waf.DataSourceRule(), + "aws_waf_rate_based_rule": waf.DataSourceRateBasedRule(), + "aws_waf_subscribed_rule_group": waf.DataSourceSubscribedRuleGroup(), + "aws_waf_web_acl": waf.DataSourceWebACL(), + + "aws_wafregional_ipset": wafregional.DataSourceIPSet(), + "aws_wafregional_rule": wafregional.DataSourceRule(), + "aws_wafregional_rate_based_rule": wafregional.DataSourceRateBasedRule(), + "aws_wafregional_subscribed_rule_group": wafregional.DataSourceSubscribedRuleGroup(), + "aws_wafregional_web_acl": wafregional.DataSourceWebACL(), "aws_wafv2_ip_set": wafv2.DataSourceIPSet(), "aws_wafv2_regex_pattern_set": wafv2.DataSourceRegexPatternSet(), diff --git a/internal/service/waf/find.go b/internal/service/waf/find.go new file mode 100644 index 00000000000..35067d06151 --- /dev/null +++ b/internal/service/waf/find.go @@ -0,0 +1,74 @@ +package waf + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/waf" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func FindSubscribedRuleGroupByNameOrMetricName(ctx context.Context, conn *waf.WAF, name string, metricName string) (*waf.SubscribedRuleGroupSummary, error) { + hasName := name != "" + hasMetricName := metricName != "" + hasMatch := false + + if !hasName && !hasMetricName { + return nil, errors.New("must specify either name or metricName") + } + + input := &waf.ListSubscribedRuleGroupsInput{} + + matchingRuleGroup := &waf.SubscribedRuleGroupSummary{} + + for { + output, err := conn.ListSubscribedRuleGroupsWithContext(ctx, input) + + if tfawserr.ErrCodeContains(err, waf.ErrCodeNonexistentItemException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + for _, ruleGroup := range output.RuleGroups { + respName := aws.StringValue(ruleGroup.Name) + respMetricName := aws.StringValue(ruleGroup.MetricName) + + if hasName && respName != name { + continue + } + if hasMetricName && respMetricName != metricName { + continue + } + if hasName && hasMetricName && (name != respName || metricName != respMetricName) { + continue + } + // Previous conditionals catch all non-matches + if hasMatch { + return nil, fmt.Errorf("multiple matches found for name %s and metricName %s", name, metricName) + } + + matchingRuleGroup = ruleGroup + hasMatch = true + } + + if output.NextMarker == nil { + break + } + input.NextMarker = output.NextMarker + } + + if !hasMatch { + return nil, fmt.Errorf("no matches found for name %s and metricName %s", name, metricName) + } + + return matchingRuleGroup, nil +} diff --git a/internal/service/waf/subscribed_rule_group.go b/internal/service/waf/subscribed_rule_group.go new file mode 100644 index 00000000000..14656420752 --- /dev/null +++ b/internal/service/waf/subscribed_rule_group.go @@ -0,0 +1,61 @@ +package waf + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + DSNameSubscribedRuleGroup = "Subscribed Rule Group Data Source" +) + +func DataSourceSubscribedRuleGroup() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceSubscribedRuleGroupRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "metric_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceSubscribedRuleGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).WAFConn + name, nameOk := d.Get("name").(string) + metricName, metricNameOk := d.Get("metric_name").(string) + + // Error out if string-assertion fails for either name or metricName + if !nameOk || !metricNameOk { + if !nameOk { + name = DSNameSubscribedRuleGroup + } + + err := errors.New("unable to read attributes") + return names.DiagError(names.WAF, names.ErrActionReading, DSNameSubscribedRuleGroup, name, err) + } + + output, err := FindSubscribedRuleGroupByNameOrMetricName(ctx, conn, name, metricName) + + if err != nil { + return names.DiagError(names.WAF, names.ErrActionReading, DSNameSubscribedRuleGroup, name, err) + } + + d.SetId(aws.StringValue(output.RuleGroupId)) + d.Set("metric_name", output.MetricName) + d.Set("name", output.Name) + + return nil +} diff --git a/internal/service/waf/subscribed_rule_group_test.go b/internal/service/waf/subscribed_rule_group_test.go new file mode 100644 index 00000000000..f638474d8c0 --- /dev/null +++ b/internal/service/waf/subscribed_rule_group_test.go @@ -0,0 +1,106 @@ +package waf_test + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/waf" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccWAFSubscribedRuleGroupDataSource_basic(t *testing.T) { + if os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_NAME") == "" { + t.Skip("Environment variable WAF_SUBSCRIBED_RULE_GROUP_NAME is not set") + } + + ruleGroupName := os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_NAME") + + if os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME") == "" { + t.Skip("Environment variable WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME is not set") + } + + metricName := os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME") + + datasourceName := "data.aws_waf_subscribed_rule_group.rulegroup" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(waf.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, waf.EndpointsID), + CheckDestroy: nil, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSubscribedRuleGroupDataSourceConfig_nonexistent, + ExpectError: regexp.MustCompile(`no matches found`), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_name(ruleGroupName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_metricName(metricName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_nameAndMetricName(ruleGroupName, metricName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_nameAndMismatchingMetricName(ruleGroupName), + ExpectError: regexp.MustCompile(`no matches found`), + }, + }, + }) +} + +func testAccSubscribedRuleGroupDataSourceConfig_name(name string) string { + return fmt.Sprintf(` +data "aws_waf_subscribed_rule_group" "rulegroup" { + name = %[1]q +} +`, name) +} + +func testAccSubscribedRuleGroupDataSourceConfig_metricName(metricName string) string { + return fmt.Sprintf(` +data "aws_waf_subscribed_rule_group" "rulegroup" { + metric_name = %[1]q +} +`, metricName) +} + +func testAccSubscribedRuleGroupDataSourceConfig_nameAndMetricName(name string, metricName string) string { + return fmt.Sprintf(` +data "aws_waf_subscribed_rule_group" "rulegroup" { + name = %[1]q + metric_name = %[2]q +} +`, name, metricName) +} + +func testAccSubscribedRuleGroupDataSourceConfig_nameAndMismatchingMetricName(name string) string { + return fmt.Sprintf(` +data "aws_waf_subscribed_rule_group" "rulegroup" { + name = %[1]q + metric_name = "tf-acc-test-does-not-exist" +} +`, name) +} + +const testAccSubscribedRuleGroupDataSourceConfig_nonexistent = ` +data "aws_waf_subscribed_rule_group" "rulegroup" { + name = "tf-acc-test-does-not-exist" +} +` diff --git a/internal/service/wafregional/find.go b/internal/service/wafregional/find.go index defdb90204c..d4677bc9840 100644 --- a/internal/service/wafregional/find.go +++ b/internal/service/wafregional/find.go @@ -1,9 +1,15 @@ package wafregional import ( + "context" + "errors" + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafregional" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func FindRegexMatchSetByID(conn *wafregional.WAFRegional, id string) (*waf.RegexMatchSet, error) { @@ -13,3 +19,65 @@ func FindRegexMatchSetByID(conn *wafregional.WAFRegional, id string) (*waf.Regex return result.RegexMatchSet, err } + +func FindSubscribedRuleGroupByNameOrMetricName(ctx context.Context, conn *wafregional.WAFRegional, name string, metricName string) (*waf.SubscribedRuleGroupSummary, error) { + hasName := name != "" + hasMetricName := metricName != "" + hasMatch := false + + if !hasName && !hasMetricName { + return nil, errors.New("must specify either name or metricName") + } + + input := &waf.ListSubscribedRuleGroupsInput{} + + matchingRuleGroup := &waf.SubscribedRuleGroupSummary{} + + for { + output, err := conn.ListSubscribedRuleGroupsWithContext(ctx, input) + + if tfawserr.ErrCodeContains(err, waf.ErrCodeNonexistentItemException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + for _, ruleGroup := range output.RuleGroups { + respName := aws.StringValue(ruleGroup.Name) + respMetricName := aws.StringValue(ruleGroup.MetricName) + + if hasName && respName != name { + continue + } + if hasMetricName && respMetricName != metricName { + continue + } + if hasName && hasMetricName && (name != respName || metricName != respMetricName) { + continue + } + // Previous conditionals catch all non-matches + if hasMatch { + return nil, fmt.Errorf("multiple matches found for name %s and metricName %s", name, metricName) + } + + matchingRuleGroup = ruleGroup + hasMatch = true + } + + if output.NextMarker == nil { + break + } + input.NextMarker = output.NextMarker + } + + if !hasMatch { + return nil, fmt.Errorf("no matches found for name %s and metricName %s", name, metricName) + } + + return matchingRuleGroup, nil +} diff --git a/internal/service/wafregional/subscribed_rule_group.go b/internal/service/wafregional/subscribed_rule_group.go new file mode 100644 index 00000000000..caf3913c8e0 --- /dev/null +++ b/internal/service/wafregional/subscribed_rule_group.go @@ -0,0 +1,61 @@ +package wafregional + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + DSNameSubscribedRuleGroup = "Subscribed Rule Group Data Source" +) + +func DataSourceSubscribedRuleGroup() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceSubscribedRuleGroupRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "metric_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceSubscribedRuleGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).WAFRegionalConn + name, nameOk := d.Get("name").(string) + metricName, metricNameOk := d.Get("metric_name").(string) + + // Error out if string-assertion fails for either name or metricName + if !nameOk || !metricNameOk { + if !nameOk { + name = DSNameSubscribedRuleGroup + } + + err := errors.New("unable to read attributes") + return names.DiagError(names.WAFRegional, names.ErrActionReading, DSNameSubscribedRuleGroup, name, err) + } + + output, err := FindSubscribedRuleGroupByNameOrMetricName(ctx, conn, name, metricName) + + if err != nil { + return names.DiagError(names.WAFRegional, names.ErrActionReading, DSNameSubscribedRuleGroup, name, err) + } + + d.SetId(aws.StringValue(output.RuleGroupId)) + d.Set("metric_name", output.MetricName) + d.Set("name", output.Name) + + return nil +} diff --git a/internal/service/wafregional/subscribed_rule_group_test.go b/internal/service/wafregional/subscribed_rule_group_test.go new file mode 100644 index 00000000000..b2282d59c3b --- /dev/null +++ b/internal/service/wafregional/subscribed_rule_group_test.go @@ -0,0 +1,106 @@ +package wafregional_test + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/wafregional" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccWAFRegionalSubscribedRuleGroupDataSource_basic(t *testing.T) { + if os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_NAME") == "" { + t.Skip("Environment variable WAF_SUBSCRIBED_RULE_GROUP_NAME is not set") + } + + ruleGroupName := os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_NAME") + + if os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME") == "" { + t.Skip("Environment variable WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME is not set") + } + + metricName := os.Getenv("WAF_SUBSCRIBED_RULE_GROUP_METRIC_NAME") + + datasourceName := "data.aws_wafregional_subscribed_rule_group.rulegroup" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(wafregional.EndpointsID, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ErrorCheck: acctest.ErrorCheck(t, wafregional.EndpointsID), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccSubscribedRuleGroupDataSourceConfig_nonexistent, + ExpectError: regexp.MustCompile(`no matches found`), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_name(ruleGroupName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_metricName(metricName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccSubscribedRuleGroupDataSourceConfig_nameAndMetricName(ruleGroupName, metricName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceName, "name", ruleGroupName), + resource.TestCheckResourceAttr(datasourceName, "metric_name", metricName), + ), + }, + { + Config: testAccDataSourceSubscribedRuleGroupDataSourceConfig_nameAndMismatchingMetricName(ruleGroupName), + ExpectError: regexp.MustCompile(`no matches found`), + }, + }, + }) +} + +func testAccSubscribedRuleGroupDataSourceConfig_name(name string) string { + return fmt.Sprintf(` +data "aws_wafregional_subscribed_rule_group" "rulegroup" { + name = %[1]q +} +`, name) +} + +func testAccSubscribedRuleGroupDataSourceConfig_metricName(metricName string) string { + return fmt.Sprintf(` +data "aws_wafregional_subscribed_rule_group" "rulegroup" { + metric_name = %[1]q +} +`, metricName) +} + +func testAccSubscribedRuleGroupDataSourceConfig_nameAndMetricName(name string, metricName string) string { + return fmt.Sprintf(` +data "aws_wafregional_subscribed_rule_group" "rulegroup" { + name = %[1]q + metric_name = %[2]q +} +`, name, metricName) +} + +func testAccDataSourceSubscribedRuleGroupDataSourceConfig_nameAndMismatchingMetricName(name string) string { + return fmt.Sprintf(` +data "aws_wafregional_subscribed_rule_group" "rulegroup" { + name = %[1]q + metric_name = "tf-acc-test-does-not-exist" +} +`, name) +} + +const testAccSubscribedRuleGroupDataSourceConfig_nonexistent = ` +data "aws_wafregional_subscribed_rule_group" "rulegroup" { + name = "tf-acc-test-does-not-exist" +} +` diff --git a/website/docs/d/waf_subscribed_rule_group.html.markdown b/website/docs/d/waf_subscribed_rule_group.html.markdown new file mode 100644 index 00000000000..cd647d1d39f --- /dev/null +++ b/website/docs/d/waf_subscribed_rule_group.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "WAF Classic" +layout: "aws" +page_title: "AWS: aws_waf_subscribed_rule_group" +description: |- + Retrieves information about a Managed WAF Rule Group from AWS Marketplace. +--- + +# Data Source: aws_waf_rule + +`aws_waf_subscribed_rule_group` retrieves information about a Managed WAF Rule Group from AWS Marketplace (needs to be subscribed to first). + +## Example Usage + +```hcl +data "aws_waf_subscribed_rule_group" "by_name" { + name = "F5 Bot Detection Signatures For AWS WAF" +} + +data "aws_waf_subscribed_rule_group" "by_metric_name" { + metric_name = "F5BotDetectionSignatures" +} + +resource "aws_waf_web_acl" "acl" { + // ... + + rules { + priority = 1 + rule_id = "${data.aws_waf_subscribed_rule_group.by_name.id}" + type = "GROUP" + } + + rules { + priority = 2 + rule_id = "${data.aws_waf_subscribed_rule_group.by_metric_name.id}" + type = "GROUP" + } +} + +``` + +## Argument Reference + +The following arguments are supported (at least one needs to be specified): + +* `name` - (Optional) The name of the WAF rule group. +* `metric_name` - (Optional) The name of the WAF rule group. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the WAF rule group. diff --git a/website/docs/d/wafregional_subscribed_rule_group.html.markdown b/website/docs/d/wafregional_subscribed_rule_group.html.markdown new file mode 100644 index 00000000000..22a9674507a --- /dev/null +++ b/website/docs/d/wafregional_subscribed_rule_group.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "WAF Classic Regional" +layout: "aws" +page_title: "AWS: aws_wafregional_subscribed_rule_group" +description: |- + retrieves information about a Managed WAF Rule Group from AWS Marketplace for use in WAF Regional. +--- + +# Data Source: aws_wafregional_rule + +`aws_wafregional_subscribed_rule_group` retrieves information about a Managed WAF Rule Group from AWS Marketplace for use in WAF Regional (needs to be subscribed to first). + +## Example Usage + +```hcl +data "aws_wafregional_subscribed_rule_group" "by_name" { + name = "F5 Bot Detection Signatures For AWS WAF" +} + +data "aws_wafregional_subscribed_rule_group" "by_metric_name" { + metric_name = "F5BotDetectionSignatures" +} + +resource "aws_wafregional_web_acl" "acl" { + // ... + + rules { + priority = 1 + rule_id = "${data.aws_wafregional_subscribed_rule_group.by_name.id}" + type = "GROUP" + } + + rules { + priority = 2 + rule_id = "${data.aws_wafregional_subscribed_rule_group.by_metric_name.id}" + type = "GROUP" + } +} + +``` + +## Argument Reference + +The following arguments are supported (at least one needs to be specified): + +* `name` - (Optional) The name of the WAF rule group. +* `metric_name` - (Optional) The name of the WAF rule group. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the WAF rule group.