From 6bf842df4ec4a70b6780d6d3ac0925b81d7b47d4 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 8 Feb 2019 11:45:49 -0500 Subject: [PATCH] resource/aws_wafregional_web_acl: Add arn attribute and logging_configuration argument This also enhances the documentation organization and adds an example of group rule usage. Reference: https://github.com/terraform-providers/terraform-provider-aws/issues/5760 Output from acceptance testing: ``` --- PASS: TestAccAWSWafRegionalWebAcl_noRules (18.11s) --- PASS: TestAccAWSWafRegionalWebAcl_createGroup (31.70s) --- PASS: TestAccAWSWafRegionalWebAcl_basic (36.22s) --- PASS: TestAccAWSWafRegionalWebAcl_createRateBased (41.71s) --- PASS: TestAccAWSWafRegionalWebAcl_changeDefaultAction (49.67s) --- PASS: TestAccAWSWafRegionalWebAcl_changeNameForceNew (51.45s) --- PASS: TestAccAWSWafRegionalWebAcl_disappears (57.65s) --- PASS: TestAccAWSWafRegionalWebAcl_changeRules (59.63s) --- PASS: TestAccAWSWafRegionalWebAcl_LoggingConfiguration (86.47s) ``` --- aws/resource_aws_wafregional_web_acl.go | 205 ++++++++++++++++++ aws/resource_aws_wafregional_web_acl_test.go | 161 ++++++++++++++ .../docs/r/wafregional_web_acl.html.markdown | 89 +++++++- 3 files changed, 443 insertions(+), 12 deletions(-) diff --git a/aws/resource_aws_wafregional_web_acl.go b/aws/resource_aws_wafregional_web_acl.go index 6389d84a549..bde2f6810ea 100644 --- a/aws/resource_aws_wafregional_web_acl.go +++ b/aws/resource_aws_wafregional_web_acl.go @@ -5,6 +5,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafregional" "github.com/hashicorp/terraform/helper/schema" @@ -19,6 +20,10 @@ func resourceAwsWafRegionalWebAcl() *schema.Resource { Delete: resourceAwsWafRegionalWebAclDelete, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "name": { Type: schema.TypeString, Required: true, @@ -37,6 +42,44 @@ func resourceAwsWafRegionalWebAcl() *schema.Resource { }, }, }, + "logging_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_destination": { + Type: schema.TypeString, + Required: true, + }, + "redacted_fields": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field_to_match": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "data": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, "metric_name": { Type: schema.TypeString, Required: true, @@ -118,6 +161,21 @@ func resourceAwsWafRegionalWebAclCreate(d *schema.ResourceData, meta interface{} } resp := out.(*waf.CreateWebACLOutput) d.SetId(*resp.WebACL.WebACLId) + + // The WAF API currently omits this, but use it when it becomes available + webACLARN := aws.StringValue(resp.WebACL.WebACLArn) + if webACLARN == "" { + webACLARN = arn.ARN{ + AccountID: meta.(*AWSClient).accountid, + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Resource: fmt.Sprintf("webacl/%s", d.Id()), + Service: "waf-regional", + }.String() + } + // Set for update + d.Set("arn", webACLARN) + return resourceAwsWafRegionalWebAclUpdate(d, meta) } @@ -144,6 +202,19 @@ func resourceAwsWafRegionalWebAclRead(d *schema.ResourceData, meta interface{}) return nil } + // The WAF API currently omits this, but use it when it becomes available + webACLARN := aws.StringValue(resp.WebACL.WebACLArn) + if webACLARN == "" { + webACLARN = arn.ARN{ + AccountID: meta.(*AWSClient).accountid, + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Resource: fmt.Sprintf("webacl/%s", d.Id()), + Service: "waf-regional", + }.String() + } + d.Set("arn", webACLARN) + if err := d.Set("default_action", flattenWafAction(resp.WebACL.DefaultAction)); err != nil { return fmt.Errorf("error setting default_action: %s", err) } @@ -153,6 +224,26 @@ func resourceAwsWafRegionalWebAclRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error setting rule: %s", err) } + getLoggingConfigurationInput := &waf.GetLoggingConfigurationInput{ + ResourceArn: aws.String(d.Get("arn").(string)), + } + loggingConfiguration := []interface{}{} + + log.Printf("[DEBUG] Getting WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), getLoggingConfigurationInput) + getLoggingConfigurationOutput, err := conn.GetLoggingConfiguration(getLoggingConfigurationInput) + + if err != nil && !isAWSErr(err, waf.ErrCodeNonexistentItemException, "") { + return fmt.Errorf("error getting WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), err) + } + + if getLoggingConfigurationOutput != nil { + loggingConfiguration = flattenWAFRegionalLoggingConfiguration(getLoggingConfigurationOutput.LoggingConfiguration) + } + + if err := d.Set("logging_configuration", loggingConfiguration); err != nil { + return fmt.Errorf("error setting logging_configuration: %s", err) + } + return nil } @@ -178,6 +269,31 @@ func resourceAwsWafRegionalWebAclUpdate(d *schema.ResourceData, meta interface{} return fmt.Errorf("Error Updating WAF Regional ACL: %s", err) } } + + if d.HasChange("logging_configuration") { + loggingConfiguration := d.Get("logging_configuration").([]interface{}) + + if len(loggingConfiguration) == 1 { + input := &waf.PutLoggingConfigurationInput{ + LoggingConfiguration: expandWAFRegionalLoggingConfiguration(loggingConfiguration, d.Get("arn").(string)), + } + + log.Printf("[DEBUG] Updating WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), input) + if _, err := conn.PutLoggingConfiguration(input); err != nil { + return fmt.Errorf("error updating WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), err) + } + } else { + input := &waf.DeleteLoggingConfigurationInput{ + ResourceArn: aws.String(d.Get("arn").(string)), + } + + log.Printf("[DEBUG] Deleting WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), input) + if _, err := conn.DeleteLoggingConfiguration(input); err != nil { + return fmt.Errorf("error deleting WAF Regional Web ACL (%s) Logging Configuration: %s", d.Id(), err) + } + } + } + return resourceAwsWafRegionalWebAclRead(d, meta) } @@ -218,3 +334,92 @@ func resourceAwsWafRegionalWebAclDelete(d *schema.ResourceData, meta interface{} } return nil } + +func expandWAFRegionalLoggingConfiguration(l []interface{}, resourceARN string) *waf.LoggingConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + loggingConfiguration := &waf.LoggingConfiguration{ + LogDestinationConfigs: []*string{ + aws.String(m["log_destination"].(string)), + }, + RedactedFields: expandWAFRegionalRedactedFields(m["redacted_fields"].([]interface{})), + ResourceArn: aws.String(resourceARN), + } + + return loggingConfiguration +} + +func expandWAFRegionalRedactedFields(l []interface{}) []*waf.FieldToMatch { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + if m["field_to_match"] == nil { + return nil + } + + redactedFields := make([]*waf.FieldToMatch, 0) + + for _, fieldToMatch := range m["field_to_match"].(*schema.Set).List() { + if fieldToMatch == nil { + continue + } + + redactedFields = append(redactedFields, expandFieldToMatch(fieldToMatch.(map[string]interface{}))) + } + + return redactedFields +} + +func flattenWAFRegionalLoggingConfiguration(loggingConfiguration *waf.LoggingConfiguration) []interface{} { + if loggingConfiguration == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "log_destination": "", + "redacted_fields": flattenWAFRegionalRedactedFields(loggingConfiguration.RedactedFields), + } + + if len(loggingConfiguration.LogDestinationConfigs) > 0 { + m["log_destination"] = aws.StringValue(loggingConfiguration.LogDestinationConfigs[0]) + } + + return []interface{}{m} +} + +func flattenWAFRegionalRedactedFields(fieldToMatches []*waf.FieldToMatch) []interface{} { + if len(fieldToMatches) == 0 { + return []interface{}{} + } + + fieldToMatchResource := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "data": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + }, + }, + } + l := make([]interface{}, len(fieldToMatches)) + + for i, fieldToMatch := range fieldToMatches { + l[i] = flattenFieldToMatch(fieldToMatch)[0] + } + + m := map[string]interface{}{ + "field_to_match": schema.NewSet(schema.HashResource(fieldToMatchResource), l), + } + + return []interface{}{m} +} diff --git a/aws/resource_aws_wafregional_web_acl_test.go b/aws/resource_aws_wafregional_web_acl_test.go index ec7457e3cac..70d7379d8ab 100644 --- a/aws/resource_aws_wafregional_web_acl_test.go +++ b/aws/resource_aws_wafregional_web_acl_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -27,6 +28,7 @@ func TestAccAWSWafRegionalWebAcl_basic(t *testing.T) { Config: testAccAWSWafRegionalWebAclConfig(wafAclName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSWafRegionalWebAclExists("aws_wafregional_web_acl.waf_acl", &v), + testAccMatchResourceAttrRegionalARN("aws_wafregional_web_acl.waf_acl", "arn", "waf-regional", regexp.MustCompile(`webacl/.+`)), resource.TestCheckResourceAttr( "aws_wafregional_web_acl.waf_acl", "default_action.#", "1"), resource.TestCheckResourceAttr( @@ -37,6 +39,8 @@ func TestAccAWSWafRegionalWebAcl_basic(t *testing.T) { "aws_wafregional_web_acl.waf_acl", "rule.#", "1"), resource.TestCheckResourceAttr( "aws_wafregional_web_acl.waf_acl", "metric_name", wafAclName), + resource.TestCheckResourceAttr( + "aws_wafregional_web_acl.waf_acl", "logging_configuration.#", "0"), ), }, }, @@ -287,6 +291,46 @@ func TestAccAWSWafRegionalWebAcl_changeRules(t *testing.T) { }) } +func TestAccAWSWafRegionalWebAcl_LoggingConfiguration(t *testing.T) { + var webACL1, webACL2, webACL3 waf.WebACL + rName := fmt.Sprintf("wafacl%s", acctest.RandString(5)) + resourceName := "aws_wafregional_web_acl.waf_acl" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSWafRegionalWebAclDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSWafRegionalWebAclConfigLoggingConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalWebAclExists(resourceName, &webACL1), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.0.redacted_fields.#", "1"), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.0.redacted_fields.0.field_to_match.#", "2"), + ), + }, + // Test logging configuration update + { + Config: testAccAWSWafRegionalWebAclConfigLoggingConfigurationUpdate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalWebAclExists(resourceName, &webACL2), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.0.redacted_fields.#", "0"), + ), + }, + // Test logging configuration removal + { + Config: testAccAWSWafRegionalWebAclConfig_noRules(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSWafRegionalWebAclExists(resourceName, &webACL3), + resource.TestCheckResourceAttr(resourceName, "logging_configuration.#", "0"), + ), + }, + }, + }) +} + // Calculates the index which isn't static because ruleId is generated as part of the test func computeWafRegionalWebAclRuleIndex(ruleId **string, priority int, ruleType string, actionType string, idx *int) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -574,3 +618,120 @@ resource "aws_wafregional_web_acl" "waf_acl" { } }`, name, name, name, name) } + +func testAccAWSWafRegionalWebAclConfigLoggingConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_wafregional_web_acl" "waf_acl" { + name = %[1]q + metric_name = %[1]q + + default_action { + type = "ALLOW" + } + + logging_configuration { + log_destination = "${aws_kinesis_firehose_delivery_stream.test.arn}" + + redacted_fields { + field_to_match { + type = "URI" + } + + field_to_match { + data = "referer" + type = "HEADER" + } + } + } +} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "private" +} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = < *NOTE:* The Kinesis Firehose Delivery Stream name must begin with `aws-waf-logs-`. See the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) for more information about enabling WAF logging. + +```hcl +resource "aws_wafregional_web_acl" "example" { + # ... other configuration ... + + logging_configuration { + log_destination = "${aws_kinesis_firehose_delivery_stream.example.arn}" + + redacted_fields { + field_to_match { + type = "URI" + } + + field_to_match { + data = "referer" + type = "HEADER" + } + } + } +} +``` + ## Argument Reference The following arguments are supported: @@ -60,32 +110,47 @@ The following arguments are supported: * `default_action` - (Required) The action that you want AWS WAF Regional to take when a request doesn't match the criteria in any of the rules that are associated with the web ACL. * `metric_name` - (Required) The name or description for the Amazon CloudWatch metric of this web ACL. * `name` - (Required) The name or description of the web ACL. -* `rule` - (Required) The rules to associate with the web ACL and the settings for each rule. +* `logging_configuration` - (Optional) Configuration block to enable WAF logging. Detailed below. +* `rule` - (Optional) Set of configuration blocks containing rules for the web ACL. Detailed below. -## Nested Fields +### `default_action` Configuration Block -### `rule` +* `type` - (Required) Specifies how you want AWS WAF Regional to respond to requests that match the settings in a rule. e.g. `ALLOW`, `BLOCK` or `COUNT` -See [docs](https://docs.aws.amazon.com/waf/latest/APIReference/API_regional_ActivatedRule.html) for all details and supported values. +### `logging_configuration` Configuration Block -#### Arguments +* `log_destination` - (Required) Amazon Resource Name (ARN) of Kinesis Firehose Delivery Stream +* `redacted_fields` - (Optional) Configuration block containing parts of the request that you want redacted from the logs. Detailed below. + +#### `redacted_fields` Configuration Block + +* `field_to_match` - (Required) Set of configuration blocks for fields to redact. Detailed below. + +##### `field_to_match` Configuration Block + +-> Additional information about this configuration can be found in the [AWS WAF Regional API Reference](https://docs.aws.amazon.com/waf/latest/APIReference/API_regional_FieldToMatch.html). + +* `data` - (Optional) When the value of `type` is `HEADER`, enter the name of the header that you want the WAF to search, for example, `User-Agent` or `Referer`. If the value of `type` is any other value, omit `data`. +* `type` - (Required) The part of the web request that you want AWS WAF to search for a specified string. e.g. `HEADER` or `METHOD` + +### `rule` Configuration Block + +-> Additional information about this configuration can be found in the [AWS WAF Regional API Reference](https://docs.aws.amazon.com/waf/latest/APIReference/API_regional_ActivatedRule.html). -* `action` - (Required) The action that CloudFront or AWS WAF takes when a web request matches the conditions in the rule. Not used if `type` is `GROUP`. -* `override_action` - (Required) Override the action that a group requests CloudFront or AWS WAF takes when a web request matches the conditions in the rule. Only used if `type` is `GROUP`. * `priority` - (Required) Specifies the order in which the rules in a WebACL are evaluated. Rules with a lower value are evaluated before rules with a higher value. * `rule_id` - (Required) ID of the associated WAF (Regional) rule (e.g. [`aws_wafregional_rule`](/docs/providers/aws/r/wafregional_rule.html)). WAF (Global) rules cannot be used. +* `action` - (Optional) Configuration block of the action that CloudFront or AWS WAF takes when a web request matches the conditions in the rule. Not used if `type` is `GROUP`. Detailed below. +* `override_action` - (Optional) Configuration block of the override the action that a group requests CloudFront or AWS WAF takes when a web request matches the conditions in the rule. Only used if `type` is `GROUP`. Detailed below. * `type` - (Optional) The rule type, either `REGULAR`, as defined by [Rule](http://docs.aws.amazon.com/waf/latest/APIReference/API_Rule.html), `RATE_BASED`, as defined by [RateBasedRule](http://docs.aws.amazon.com/waf/latest/APIReference/API_RateBasedRule.html), or `GROUP`, as defined by [RuleGroup](https://docs.aws.amazon.com/waf/latest/APIReference/API_RuleGroup.html). The default is REGULAR. If you add a RATE_BASED rule, you need to set `type` as `RATE_BASED`. If you add a GROUP rule, you need to set `type` as `GROUP`. -### `default_action` / `action` - -#### Arguments +#### `action` / `override_action` Configuration Block -* `type` - (Required) Specifies how you want AWS WAF Regional to respond to requests that match the settings in a rule. - e.g. `ALLOW`, `BLOCK` or `COUNT` +* `type` - (Required) Specifies how you want AWS WAF Regional to respond to requests that match the settings in a rule. e.g. `ALLOW`, `BLOCK` or `COUNT` ## Attributes Reference In addition to all arguments above, the following attributes are exported: +* `arn` - Amazon Resource Name (ARN) of the WAF Regional WebACL. * `id` - The ID of the WAF Regional WebACL.