Skip to content

Commit

Permalink
Merge pull request #1339 from cloudflare/ruleset-support-for-rewritin…
Browse files Browse the repository at this point in the history
…g-headers

Ruleset support for rewriting response headers
  • Loading branch information
jacobbednarz authored Dec 11, 2021
2 parents a187d1d + 674b1c9 commit ba70ea1
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .changelog/1339.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/cloudflare_ruleset: add support for rewriting HTTP response headers
```
196 changes: 193 additions & 3 deletions cloudflare/resource_cloudflare_ruleset_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,74 @@
package cloudflare

import (
"context"
"fmt"
"log"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/pkg/errors"
)

func init() {
resource.AddTestSweepers("cloudflare_ruleset", &resource.Sweeper{
Name: "cloudflare_ruleset",
F: testSweepCloudflareRuleset,
})
}

func testSweepCloudflareRuleset(r string) error {
client, clientErr := sharedClient()
if clientErr != nil {
log.Printf("[ERROR] Failed to create Cloudflare client: %s", clientErr)
}

// Clean up the account level rulesets
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
if accountID == "" {
return errors.New("CLOUDFLARE_ACCOUNT_ID must be set")
}

accountRulesets, accountRulesetsErr := client.ListAccountRulesets(context.Background(), accountID)
if accountRulesetsErr != nil {
log.Printf("[ERROR] Failed to fetch Cloudflare Account Rulesets: %s", accountRulesetsErr)
}

if len(accountRulesets) == 0 {
log.Print("[DEBUG] No Cloudflare Account Rulesets to sweep")
return nil
}

for _, ruleset := range accountRulesets {
log.Printf("[INFO] Deleting Cloudflare Account Ruleset ID: %s", ruleset.ID)
client.DeleteAccountRuleset(context.Background(), accountID, ruleset.ID)
}

// .. and zone level rulesets
zone := os.Getenv("CLOUDFLARE_ZONE_ID")
if zone == "" {
return errors.New("CLOUDFLARE_ZONE_ID must be set")
}

zoneRulesets, zoneRulesetsErr := client.ListZoneRulesets(context.Background(), zoneID)
if zoneRulesetsErr != nil {
log.Printf("[ERROR] Failed to fetch Cloudflare Zone Rulesets: %s", zoneRulesetsErr)
}

if len(zoneRulesets) == 0 {
log.Print("[DEBUG] No Cloudflare Zone Rulesets to sweep")
return nil
}

for _, ruleset := range zoneRulesets {
log.Printf("[INFO] Deleting Cloudflare Zone Ruleset ID: %s", ruleset.ID)
client.DeleteZoneRuleset(context.Background(), zoneID, ruleset.ID)
}

return nil
}

func TestAccCloudflareRuleset_WAFBasic(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
Expand Down Expand Up @@ -728,6 +789,48 @@ func TestAccCloudflareRuleset_TransformationRuleURIQuery(t *testing.T) {
})
}

func TestAccCloudflareRuleset_TransformHTTPResponseHeaders(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
// misleading state error messages.
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
defer func(apiToken string) {
os.Setenv("CLOUDFLARE_API_TOKEN", apiToken)
}(os.Getenv("CLOUDFLARE_API_TOKEN"))
os.Setenv("CLOUDFLARE_API_TOKEN", "")
}

t.Parallel()
rnd := generateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_ruleset." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareRulesetExposedCredentialCheck(rnd, "example exposed credential check", accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", "example exposed credential check"),
resource.TestCheckResourceAttr(resourceName, "description", "This ruleset includes a rule checking for exposed credentials."),
resource.TestCheckResourceAttr(resourceName, "kind", "custom"),
resource.TestCheckResourceAttr(resourceName, "phase", "http_request_firewall_custom"),

resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action", "log"),
resource.TestCheckResourceAttr(resourceName, "rules.0.expression", "http.request.method == \"POST\" && http.request.uri == \"/login.php\""),
resource.TestCheckResourceAttr(resourceName, "rules.0.description", "example exposed credential check"),
resource.TestCheckResourceAttr(resourceName, "rules.0.exposed_credential_check.#", "1"),

resource.TestCheckResourceAttr(resourceName, "rules.0.exposed_credential_check.0.username_expression", "url_decode(http.request.body.form[\"username\"][0])"),
resource.TestCheckResourceAttr(resourceName, "rules.0.exposed_credential_check.0.password_expression", "url_decode(http.request.body.form[\"password\"][0])"),
),
},
},
})
}

func TestAccCloudflareRuleset_TransformationRuleURIPathAndQueryCombination(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
Expand Down Expand Up @@ -771,7 +874,7 @@ func TestAccCloudflareRuleset_TransformationRuleURIPathAndQueryCombination(t *te
})
}

func TestAccCloudflareRuleset_TransformationRuleHeaders(t *testing.T) {
func TestAccCloudflareRuleset_TransformationRuleRequestHeaders(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
// misleading state error messages.
Expand All @@ -793,7 +896,7 @@ func TestAccCloudflareRuleset_TransformationRuleHeaders(t *testing.T) {
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareRulesetTransformationRuleHeaders(rnd, "transform rule for headers", zoneID, zoneName),
Config: testAccCheckCloudflareRulesetTransformationRuleRequestHeaders(rnd, "transform rule for headers", zoneID, zoneName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", "transform rule for headers"),
resource.TestCheckResourceAttr(resourceName, "description", rnd+" ruleset description"),
Expand Down Expand Up @@ -821,6 +924,56 @@ func TestAccCloudflareRuleset_TransformationRuleHeaders(t *testing.T) {
})
}

func TestAccCloudflareRuleset_TransformationRuleResponseHeaders(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
// misleading state error messages.
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
defer func(apiToken string) {
os.Setenv("CLOUDFLARE_API_TOKEN", apiToken)
}(os.Getenv("CLOUDFLARE_API_TOKEN"))
os.Setenv("CLOUDFLARE_API_TOKEN", "")
}

t.Parallel()
rnd := generateRandomResourceName()
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
zoneName := os.Getenv("CLOUDFLARE_DOMAIN")
resourceName := "cloudflare_ruleset." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareRulesetTransformationRuleResponseHeaders(rnd, "transform rule for headers", zoneID, zoneName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", "transform rule for headers"),
resource.TestCheckResourceAttr(resourceName, "description", rnd+" ruleset description"),
resource.TestCheckResourceAttr(resourceName, "kind", "zone"),
resource.TestCheckResourceAttr(resourceName, "phase", "http_response_headers_transform"),

resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action", "rewrite"),

resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.#", "3"),

resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.0.name", "example1"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.0.value", "my-http-header-value1"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.0.operation", "set"),

resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.1.name", "example2"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.1.operation", "set"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.1.expression", "cf.zone.name"),

resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.2.name", "example3"),
resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.headers.2.operation", "remove"),
),
},
},
})
}

func TestAccCloudflareRuleset_ActionParametersMultipleSkips(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF
// service does not yet support the API tokens and it results in
Expand Down Expand Up @@ -1512,7 +1665,7 @@ func testAccCheckCloudflareRulesetTransformationRuleURIQuery(rnd, name, zoneID,
}`, rnd, name, zoneID, zoneName)
}

func testAccCheckCloudflareRulesetTransformationRuleHeaders(rnd, name, zoneID, zoneName string) string {
func testAccCheckCloudflareRulesetTransformationRuleRequestHeaders(rnd, name, zoneID, zoneName string) string {
return fmt.Sprintf(`
resource "cloudflare_ruleset" "%[1]s" {
zone_id = "%[3]s"
Expand Down Expand Up @@ -1549,6 +1702,43 @@ func testAccCheckCloudflareRulesetTransformationRuleHeaders(rnd, name, zoneID, z
}`, rnd, name, zoneID, zoneName)
}

func testAccCheckCloudflareRulesetTransformationRuleResponseHeaders(rnd, name, zoneID, zoneName string) string {
return fmt.Sprintf(`
resource "cloudflare_ruleset" "%[1]s" {
zone_id = "%[3]s"
name = "%[2]s"
description = "%[1]s ruleset description"
kind = "zone"
phase = "http_response_headers_transform"
rules {
action = "rewrite"
action_parameters {
headers {
name = "example1"
operation = "set"
value = "my-http-header-value1"
}
headers {
name = "example2"
operation = "set"
expression = "cf.zone.name"
}
headers {
name = "example3"
operation = "remove"
}
}
expression = "true"
description = "example header transformation rule"
enabled = false
}
}`, rnd, name, zoneID, zoneName)
}

func testAccCheckCloudflareRulesetManagedWAFPayloadLogging(rnd, name, zoneID, zoneName string) string {
return fmt.Sprintf(`
resource "cloudflare_ruleset" "%[1]s" {
Expand Down
2 changes: 1 addition & 1 deletion website/docs/r/ruleset.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ The following arguments are supported:
* `description` - (Required) Brief summary of the ruleset and its intended use.
* `kind` - (Required) Type of Ruleset to create. Valid values are `"custom"`, `"managed"`, `"root"`, `"schema"` or `"zone"`.
* `name` - (Required) Name of the ruleset.
* `phase` - (Required) Point in the request/response lifecycle where the ruleset will be created. Valid values are `"ddos_l4"`, `"ddos_l7"`, `"http_request_firewall_custom"`, `"http_request_firewall_managed"`, `"http_request_late_transform"`, `"http_request_main"`, `"http_request_sanitize"`, `"http_request_transform"`, `"http_response_firewall_managed"`, `"magic_transit"`, or `"http_ratelimit"`.
* `phase` - (Required) Point in the request/response lifecycle where the ruleset will be created. Valid values are `"ddos_l4"`, `"ddos_l7"`, `"http_request_firewall_custom"`, `"http_request_firewall_managed"`, `"http_request_late_transform"`, `"http_response_headers_transform"`, `"http_request_main"`, `"http_request_sanitize"`, `"http_request_transform"`, `"http_response_firewall_managed"`, `"magic_transit"`, or `"http_ratelimit"`.
* `rules` - (Required) List of rules to apply to the ruleset (refer to the [nested schema](#nestedblock--rules)).
* `shareable_entitlement_name` - (Optional) Name of entitlement that is shareable between entities.
* `zone_id` - (Optional) The ID of the zone where the ruleset is being created. Conflicts with `"account_id"`.
Expand Down

0 comments on commit ba70ea1

Please sign in to comment.