diff --git a/aws/provider.go b/aws/provider.go index 964c86e3bbc..b7054c3a081 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -558,6 +558,7 @@ func Provider() terraform.ResourceProvider { "aws_wafregional_ipset": resourceAwsWafRegionalIPSet(), "aws_wafregional_sql_injection_match_set": resourceAwsWafRegionalSqlInjectionMatchSet(), "aws_wafregional_xss_match_set": resourceAwsWafRegionalXssMatchSet(), + "aws_wafregional_rule": resourceAwsWafRegionalRule(), "aws_batch_compute_environment": resourceAwsBatchComputeEnvironment(), "aws_batch_job_definition": resourceAwsBatchJobDefinition(), "aws_batch_job_queue": resourceAwsBatchJobQueue(), diff --git a/aws/resource_aws_wafregional_rule.go b/aws/resource_aws_wafregional_rule.go new file mode 100644 index 00000000000..f470cba4d59 --- /dev/null +++ b/aws/resource_aws_wafregional_rule.go @@ -0,0 +1,187 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/service/wafregional" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/waf" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsWafRegionalRule() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsWafRegionalRuleCreate, + Read: resourceAwsWafRegionalRuleRead, + Update: resourceAwsWafRegionalRuleUpdate, + Delete: resourceAwsWafRegionalRuleDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "metric_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "predicate": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "negated": &schema.Schema{ + Type: schema.TypeBool, + Required: true, + }, + "data_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "IPMatch", + "ByteMatch", + "SqlInjectionMatch", + "SizeConstraint", + "XssMatch", + }, false), + }, + }, + }, + }, + }, + } +} + +func resourceAwsWafRegionalRuleCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).wafregionalconn + region := meta.(*AWSClient).region + + wr := newWafRegionalRetryer(conn, region) + out, err := wr.RetryWithToken(func(token *string) (interface{}, error) { + params := &waf.CreateRuleInput{ + ChangeToken: token, + MetricName: aws.String(d.Get("metric_name").(string)), + Name: aws.String(d.Get("name").(string)), + } + + return conn.CreateRule(params) + }) + if err != nil { + return err + } + resp := out.(*waf.CreateRuleOutput) + d.SetId(*resp.Rule.RuleId) + return resourceAwsWafRegionalRuleUpdate(d, meta) +} + +func resourceAwsWafRegionalRuleRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).wafregionalconn + + params := &waf.GetRuleInput{ + RuleId: aws.String(d.Id()), + } + + resp, err := conn.GetRule(params) + if err != nil { + if isAWSErr(err, wafregional.ErrCodeWAFNonexistentItemException, "") { + log.Printf("[WARN] WAF Rule (%s) not found, error code (404)", d.Id()) + d.SetId("") + return nil + } + + return err + } + + d.Set("predicate", flattenWafPredicates(resp.Rule.Predicates)) + d.Set("name", resp.Rule.Name) + d.Set("metric_name", resp.Rule.MetricName) + + return nil +} + +func resourceAwsWafRegionalRuleUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("predicate") { + o, n := d.GetChange("predicate") + oldP, newP := o.(*schema.Set).List(), n.(*schema.Set).List() + + err := updateWafRegionalRuleResource(d.Id(), oldP, newP, meta) + if err != nil { + return fmt.Errorf("Error Updating WAF Rule: %s", err) + } + } + return resourceAwsWafRegionalRuleRead(d, meta) +} + +func resourceAwsWafRegionalRuleDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).wafregionalconn + region := meta.(*AWSClient).region + + oldPredicates := d.Get("predicate").(*schema.Set).List() + if len(oldPredicates) > 0 { + noPredicates := []interface{}{} + err := updateWafRegionalRuleResource(d.Id(), oldPredicates, noPredicates, meta) + if err != nil { + return fmt.Errorf("Error Removing WAF Rule Predicates: %s", err) + } + } + + wr := newWafRegionalRetryer(conn, region) + _, err := wr.RetryWithToken(func(token *string) (interface{}, error) { + req := &waf.DeleteRuleInput{ + ChangeToken: token, + RuleId: aws.String(d.Id()), + } + log.Printf("[INFO] Deleting WAF Rule") + return conn.DeleteRule(req) + }) + if err != nil { + return fmt.Errorf("Error deleting WAF Rule: %s", err) + } + + return nil +} + +//func updateWafRegionalRuleResource(d *schema.ResourceData, meta interface{}, ChangeAction string) error { +func updateWafRegionalRuleResource(id string, oldP, newP []interface{}, meta interface{}) error { + conn := meta.(*AWSClient).wafregionalconn + region := meta.(*AWSClient).region + + wr := newWafRegionalRetryer(conn, region) + _, err := wr.RetryWithToken(func(token *string) (interface{}, error) { + req := &waf.UpdateRuleInput{ + ChangeToken: token, + RuleId: aws.String(id), + Updates: diffWafRulePredicates(oldP, newP), + } + + return conn.UpdateRule(req) + }) + + if err != nil { + return fmt.Errorf("Error Updating WAF Rule: %s", err) + } + + return nil +} + +func flattenWafPredicates(ts []*waf.Predicate) []interface{} { + out := make([]interface{}, len(ts), len(ts)) + for i, p := range ts { + m := make(map[string]interface{}) + m["negated"] = *p.Negated + m["type"] = *p.Type + m["data_id"] = *p.DataId + out[i] = m + } + return out +} diff --git a/aws/resource_aws_wafregional_rule_test.go b/aws/resource_aws_wafregional_rule_test.go new file mode 100644 index 00000000000..41d99c71a83 --- /dev/null +++ b/aws/resource_aws_wafregional_rule_test.go @@ -0,0 +1,375 @@ +package aws + +import ( + "fmt" + "testing" + + "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/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSWafRegionalRule_basic(t *testing.T) { + var v waf.Rule + wafRuleName := fmt.Sprintf("wafrule%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRegionalRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSWafRegionalRuleConfig(wafRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &v), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "name", wafRuleName), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "predicate.#", "1"), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "metric_name", wafRuleName), + ), + }, + }, + }) +} + +func TestAccAWSWafRegionalRule_changeNameForceNew(t *testing.T) { + var before, after waf.Rule + wafRuleName := fmt.Sprintf("wafrule%s", acctest.RandString(5)) + wafRuleNewName := fmt.Sprintf("wafrulenew%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRegionalIPSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSWafRegionalRuleConfig(wafRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &before), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "name", wafRuleName), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "predicate.#", "1"), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "metric_name", wafRuleName), + ), + }, + { + Config: testAccAWSWafRegionalRuleConfigChangeName(wafRuleNewName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &after), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "name", wafRuleNewName), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "predicate.#", "1"), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "metric_name", wafRuleNewName), + ), + }, + }, + }) +} + +func TestAccAWSWafRegionalRule_disappears(t *testing.T) { + var v waf.Rule + wafRuleName := fmt.Sprintf("wafrule%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRegionalRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSWafRegionalRuleConfig(wafRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &v), + testAccCheckAWSWafRegionalRuleDisappears(&v), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSWafRegionalRule_noPredicates(t *testing.T) { + var v waf.Rule + wafRuleName := fmt.Sprintf("wafrule%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRegionalRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSWafRegionalRule_noPredicates(wafRuleName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &v), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "name", wafRuleName), + resource.TestCheckResourceAttr( + "aws_wafregional_rule.wafrule", "predicate.#", "0"), + ), + }, + }, + }) +} + +func TestAccAWSWafRegionalRule_changePredicates(t *testing.T) { + var ipset waf.IPSet + var xssMatchSet waf.XssMatchSet + + var before, after waf.Rule + var idx int + ruleName := fmt.Sprintf("wafrule%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSWafRegionalRuleConfig(ruleName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSWafRegionalIPSetExists("aws_wafregional_ipset.ipset", &ipset), + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &before), + resource.TestCheckResourceAttr("aws_wafregional_rule.wafrule", "name", ruleName), + resource.TestCheckResourceAttr("aws_wafregional_rule.wafrule", "predicate.#", "1"), + computeWafRegionalRulePredicate(&ipset.IPSetId, false, "IPMatch", &idx), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.negated", &idx, "false"), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.type", &idx, "IPMatch"), + ), + }, + { + Config: testAccAWSWafRegionalRule_changePredicates(ruleName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSWafRegionalXssMatchSetExists("aws_wafregional_xss_match_set.xss_match_set", &xssMatchSet), + testAccCheckAWSWafRegionalRuleExists("aws_wafregional_rule.wafrule", &after), + resource.TestCheckResourceAttr("aws_wafregional_rule.wafrule", "name", ruleName), + resource.TestCheckResourceAttr("aws_wafregional_rule.wafrule", "predicate.#", "2"), + computeWafRegionalRulePredicate(&xssMatchSet.XssMatchSetId, true, "XssMatch", &idx), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.negated", &idx, "true"), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.type", &idx, "XssMatch"), + computeWafRegionalRulePredicate(&ipset.IPSetId, true, "IPMatch", &idx), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.negated", &idx, "true"), + testCheckResourceAttrWithIndexesAddr("aws_wafregional_rule.wafrule", "predicate.%d.type", &idx, "IPMatch"), + ), + }, + }, + }) +} + +// Calculates the index which isn't static because dataId is generated as part of the test +func computeWafRegionalRulePredicate(dataId **string, negated bool, pType string, idx *int) resource.TestCheckFunc { + return func(s *terraform.State) error { + predicateResource := resourceAwsWafRegionalRule().Schema["predicate"].Elem.(*schema.Resource) + m := map[string]interface{}{ + "data_id": **dataId, + "negated": negated, + "type": pType, + } + + f := schema.HashResource(predicateResource) + *idx = f(m) + + return nil + } +} + +func testAccCheckAWSWafRegionalRuleDisappears(v *waf.Rule) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).wafregionalconn + region := testAccProvider.Meta().(*AWSClient).region + + wr := newWafRegionalRetryer(conn, region) + _, err := wr.RetryWithToken(func(token *string) (interface{}, error) { + req := &waf.UpdateRuleInput{ + ChangeToken: token, + RuleId: v.RuleId, + } + + for _, predicate := range v.Predicates { + predicate := &waf.RuleUpdate{ + Action: aws.String("DELETE"), + Predicate: &waf.Predicate{ + Negated: predicate.Negated, + Type: predicate.Type, + DataId: predicate.DataId, + }, + } + req.Updates = append(req.Updates, predicate) + } + + return conn.UpdateRule(req) + }) + if err != nil { + return fmt.Errorf("Error Updating WAF Rule: %s", err) + } + + _, err = wr.RetryWithToken(func(token *string) (interface{}, error) { + opts := &waf.DeleteRuleInput{ + ChangeToken: token, + RuleId: v.RuleId, + } + return conn.DeleteRule(opts) + }) + if err != nil { + return fmt.Errorf("Error Deleting WAF Rule: %s", err) + } + return nil + } +} + +func testAccCheckAWSWafRegionalRuleDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_wafregional_rule" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).wafregionalconn + resp, err := conn.GetRule( + &waf.GetRuleInput{ + RuleId: aws.String(rs.Primary.ID), + }) + + if err == nil { + if *resp.Rule.RuleId == rs.Primary.ID { + return fmt.Errorf("WAF Rule %s still exists", rs.Primary.ID) + } + } + + // Return nil if the Rule is already destroyed + if isAWSErr(err, wafregional.ErrCodeWAFNonexistentItemException, "") { + return nil + } + + return err + } + + return nil +} + +func testAccCheckAWSWafRegionalRuleExists(n string, v *waf.Rule) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No WAF Rule ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).wafregionalconn + resp, err := conn.GetRule(&waf.GetRuleInput{ + RuleId: aws.String(rs.Primary.ID), + }) + + if err != nil { + return err + } + + if *resp.Rule.RuleId == rs.Primary.ID { + *v = *resp.Rule + return nil + } + + return fmt.Errorf("WAF Rule (%s) not found", rs.Primary.ID) + } +} + +func testAccAWSWafRegionalRuleConfig(name string) string { + return fmt.Sprintf(` +resource "aws_wafregional_ipset" "ipset" { + name = "%s" + + ip_set_descriptor { + type = "IPV4" + value = "192.0.7.0/24" + } +} + +resource "aws_wafregional_rule" "wafrule" { + name = "%s" + metric_name = "%s" + + predicate { + data_id = "${aws_wafregional_ipset.ipset.id}" + negated = false + type = "IPMatch" + } +}`, name, name, name) +} + +func testAccAWSWafRegionalRuleConfigChangeName(name string) string { + return fmt.Sprintf(` +resource "aws_wafregional_ipset" "ipset" { + name = "%s" + + ip_set_descriptor { + type = "IPV4" + value = "192.0.7.0/24" + } +} + +resource "aws_wafregional_rule" "wafrule" { + name = "%s" + metric_name = "%s" + + predicate { + data_id = "${aws_wafregional_ipset.ipset.id}" + negated = false + type = "IPMatch" + } +}`, name, name, name) +} + +func testAccAWSWafRegionalRule_noPredicates(name string) string { + return fmt.Sprintf(` +resource "aws_wafregional_rule" "wafrule" { + name = "%s" + metric_name = "%s" +} +`, name, name) +} + +func testAccAWSWafRegionalRule_changePredicates(name string) string { + return fmt.Sprintf(` +resource "aws_wafregional_ipset" "ipset" { + name = "%s" + + ip_set_descriptor { + type = "IPV4" + value = "192.0.7.0/24" + } +} + +resource "aws_wafregional_xss_match_set" "xss_match_set" { + name = "%s" + xss_match_tuple { + text_transformation = "NONE" + field_to_match { + type = "URI" + } + } +} + +resource "aws_wafregional_rule" "wafrule" { + name = "%s" + metric_name = "%s" + + predicate { + data_id = "${aws_wafregional_xss_match_set.xss_match_set.id}" + negated = true + type = "XssMatch" + } + + predicate { + data_id = "${aws_wafregional_ipset.ipset.id}" + negated = true + type = "IPMatch" + } +}`, name, name, name, name) +} diff --git a/website/aws.erb b/website/aws.erb index d8232925d58..c9dfcc8a0a5 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1523,6 +1523,10 @@ aws_wafregional_xss_match_set + > + aws_wafregional_rule + + diff --git a/website/docs/r/wafregional_rule.html.markdown b/website/docs/r/wafregional_rule.html.markdown new file mode 100644 index 00000000000..e879cf89299 --- /dev/null +++ b/website/docs/r/wafregional_rule.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "aws" +page_title: "AWS: wafregional_rule" +sidebar_current: "docs-aws-resource-wafregional-rule" +description: |- + Provides an AWS WAF Regional rule resource for use with ALB. +--- + +# aws\_wafregional\_rule + +Provides an WAF Regional Rule Resource for use with Application Load Balancer. + +## Example Usage + +```hcl +resource "aws_wafregional_ipset" "ipset" { + name = "tfIPSet" + + ip_set_descriptor { + type = "IPV4" + value = "192.0.7.0/24" + } +} + +resource "aws_wafregional_rule" "wafrule" { + name = "tfWAFRule" + metric_name = "tfWAFRule" + + predicate { + type = "IPMatch" + data_id = "${aws_wafregional_ipset.ipset.id}" + negated = false + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name or description of the rule. +* `metric_name` - (Required) The name or description for the Amazon CloudWatch metric of this rule. +* `predicate` - (Optional) The `ByteMatchSet`, `IPSet`, `SizeConstraintSet`, `SqlInjectionMatchSet`, or `XssMatchSet` objects to include in a rule. + +## Nested Fields + +### `predicate` + +See [docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafregional-rule-predicates.html) + +#### Arguments + +* `type` - (Required) The type of predicate in a rule, such as an IPSet (IPMatch) +* `data_id` - (Required) The unique identifier of a predicate, such as the ID of a `ByteMatchSet` or `IPSet`. +* `negated` - (Required) Whether to use the settings or the negated settings that you specified in the `ByteMatchSet`, `IPSet`, `SizeConstraintSet`, `SqlInjectionMatchSet`, or `XssMatchSet` objects. + +## Remarks + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the WAF Regional Rule.