diff --git a/.changelog/2177.txt b/.changelog/2177.txt new file mode 100644 index 0000000000..93558357f7 --- /dev/null +++ b/.changelog/2177.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/cloudflare_ruleset: add support for `score_per_period` and `score_response_header_name` +``` diff --git a/docs/resources/ruleset.md b/docs/resources/ruleset.md index 2fd8dfe2f8..c98ca08405 100644 --- a/docs/resources/ruleset.md +++ b/docs/resources/ruleset.md @@ -821,6 +821,8 @@ Optional: - `period` (Number) The period of time to consider (in seconds) when evaluating the request rate. - `requests_per_period` (Number) The number of requests over the period of time that will trigger the Rate Limiting rule. - `requests_to_origin` (Boolean) Whether to include requests to origin within the Rate Limiting count. +- `score_per_period` (Number) The maximum aggregate score over the period of time that will trigger Rate Limiting rule. +- `score_response_header_name` (String) Name of HTTP header in the response, set by the origin server, with the score for the current request. ## Import diff --git a/internal/sdkv2provider/resource_cloudflare_ruleset.go b/internal/sdkv2provider/resource_cloudflare_ruleset.go index 65ee362212..2e028dc4a5 100644 --- a/internal/sdkv2provider/resource_cloudflare_ruleset.go +++ b/internal/sdkv2provider/resource_cloudflare_ruleset.go @@ -621,12 +621,14 @@ func buildStateFromRulesetRules(rules []cloudflare.RulesetRule) interface{} { var rateLimit []map[string]interface{} rateLimit = append(rateLimit, map[string]interface{}{ - "characteristics": r.RateLimit.Characteristics, - "period": r.RateLimit.Period, - "requests_per_period": r.RateLimit.RequestsPerPeriod, - "mitigation_timeout": r.RateLimit.MitigationTimeout, - "counting_expression": r.RateLimit.CountingExpression, - "requests_to_origin": r.RateLimit.RequestsToOrigin, + "characteristics": r.RateLimit.Characteristics, + "period": r.RateLimit.Period, + "requests_per_period": r.RateLimit.RequestsPerPeriod, + "score_per_period": r.RateLimit.ScorePerPeriod, + "score_response_header_name": r.RateLimit.ScoreResponseHeaderName, + "mitigation_timeout": r.RateLimit.MitigationTimeout, + "counting_expression": r.RateLimit.CountingExpression, + "requests_to_origin": r.RateLimit.RequestsToOrigin, }) rule["ratelimit"] = rateLimit @@ -1264,6 +1266,10 @@ func buildRule(d *schema.ResourceData, resourceRule map[string]interface{}, rule rule.RateLimit.Period = pValue.(int) case "requests_per_period": rule.RateLimit.RequestsPerPeriod = pValue.(int) + case "score_per_period": + rule.RateLimit.ScorePerPeriod = pValue.(int) + case "score_response_header_name": + rule.RateLimit.ScoreResponseHeaderName = pValue.(string) case "mitigation_timeout": rule.RateLimit.MitigationTimeout = pValue.(int) case "counting_expression": diff --git a/internal/sdkv2provider/resource_cloudflare_ruleset_test.go b/internal/sdkv2provider/resource_cloudflare_ruleset_test.go index 665876220f..e0b7f7a747 100644 --- a/internal/sdkv2provider/resource_cloudflare_ruleset_test.go +++ b/internal/sdkv2provider/resource_cloudflare_ruleset_test.go @@ -734,6 +734,53 @@ func TestAccCloudflareRuleset_RateLimit(t *testing.T) { }) } +func TestAccCloudflareRuleset_RateLimitScorePerPeriod(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") != "" { + t.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + t.Parallel() + rnd := generateRandomResourceName() + zoneName := os.Getenv("CLOUDFLARE_DOMAIN") + resourceName := "cloudflare_ruleset." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareRulesetRateLimitScorePerPeriod(rnd, "example HTTP rate limit by header score", zoneID, zoneName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "example HTTP rate limit by header score"), + resource.TestCheckResourceAttr(resourceName, "description", rnd+" ruleset description"), + resource.TestCheckResourceAttr(resourceName, "kind", "zone"), + resource.TestCheckResourceAttr(resourceName, "phase", "http_ratelimit"), + + resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action", "block"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.response.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.response.0.status_code", "418"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.response.0.content_type", "text/plain"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.response.0.content", "test content"), + resource.TestCheckResourceAttr(resourceName, "rules.0.expression", "(http.request.uri.path matches \"^/api/\")"), + resource.TestCheckResourceAttr(resourceName, "rules.0.description", "example http rate limit"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.#", "1"), + + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.characteristics.#", "2"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.period", "60"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.score_per_period", "400"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.score_response_header_name", "my-score"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.mitigation_timeout", "60"), + resource.TestCheckResourceAttr(resourceName, "rules.0.ratelimit.0.requests_to_origin", "true"), + ), + }, + }, + }) +} + func TestAccCloudflareRuleset_PreserveRuleIDs(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 @@ -2608,6 +2655,42 @@ func testAccCheckCloudflareRulesetRateLimit(rnd, name, zoneID, zoneName string) }`, rnd, name, zoneID, zoneName) } +func testAccCheckCloudflareRulesetRateLimitScorePerPeriod(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_ratelimit" + + rules { + action = "block" + action_parameters { + response { + status_code = 418 + content_type = "text/plain" + content = "test content" + } + } + ratelimit { + characteristics = [ + "cf.colo.id", + "ip.src" + ] + period = 60 + score_per_period = 400 + score_response_header_name = "my-score" + mitigation_timeout = 60 + requests_to_origin = true + } + expression = "(http.request.uri.path matches \"^/api/\")" + description = "example http rate limit" + enabled = true + } + }`, rnd, name, zoneID, zoneName) +} + func testAccCheckCloudflareRulesetTwoCustomRules(rnd, zoneID string) string { return fmt.Sprintf(` resource "cloudflare_ruleset" "%[1]s" { diff --git a/internal/sdkv2provider/schema_cloudflare_ruleset.go b/internal/sdkv2provider/schema_cloudflare_ruleset.go index 238c99dbb9..ff26cc8ad1 100644 --- a/internal/sdkv2provider/schema_cloudflare_ruleset.go +++ b/internal/sdkv2provider/schema_cloudflare_ruleset.go @@ -917,6 +917,16 @@ func resourceCloudflareRulesetSchema() map[string]*schema.Schema { Optional: true, Description: "The number of requests over the period of time that will trigger the Rate Limiting rule.", }, + "score_per_period": { + Type: schema.TypeInt, + Optional: true, + Description: "The maximum aggregate score over the period of time that will trigger Rate Limiting rule.", + }, + "score_response_header_name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of HTTP header in the response, set by the origin server, with the score for the current request.", + }, "mitigation_timeout": { Type: schema.TypeInt, Optional: true,