diff --git a/cloudflare/resource_cloudflare_rate_limit.go b/cloudflare/resource_cloudflare_rate_limit.go index fc95731453..33d432fb02 100644 --- a/cloudflare/resource_cloudflare_rate_limit.go +++ b/cloudflare/resource_cloudflare_rate_limit.go @@ -56,12 +56,12 @@ func resourceCloudflareRateLimit() *schema.Resource { "mode": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringInSlice([]string{"simulate", "ban"}, true), + ValidateFunc: validation.StringInSlice([]string{"simulate", "ban", "challenge", "js_challenge"}, true), }, "timeout": { Type: schema.TypeInt, - Required: true, + Optional: true, ValidateFunc: validation.IntBetween(1, 86400), }, @@ -198,10 +198,15 @@ func resourceCloudflareRateLimit() *schema.Resource { func resourceCloudflareRateLimitCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*cloudflare.API) + rateLimitAction, err := expandRateLimitAction(d) + if err != nil { + return errors.Wrap(err, "error expanding rate limit action") + } + newRateLimit := cloudflare.RateLimit{ Threshold: d.Get("threshold").(int), Period: d.Get("period").(int), - Action: expandRateLimitAction(d), + Action: rateLimitAction, } newRateLimitMatch, err := expandRateLimitTrafficMatcher(d) @@ -224,6 +229,12 @@ func resourceCloudflareRateLimitCreate(d *schema.ResourceData, meta interface{}) newRateLimit.Correlate, _ = expandRateLimitCorrelate(d) + newRateLimitAction, err := expandRateLimitAction(d) + if err != nil { + return err + } + newRateLimit.Action = newRateLimitAction + zoneName := d.Get("zone").(string) zoneId, err := client.ZoneIDByName(zoneName) if err != nil { @@ -256,11 +267,22 @@ func resourceCloudflareRateLimitUpdate(d *schema.ResourceData, meta interface{}) zoneId := d.Get("zone_id").(string) rateLimitId := d.Id() + rateLimitAction, err := expandRateLimitAction(d) + if err != nil { + return errors.Wrap(err, "error expanding rate limit action") + } + updatedRateLimit := cloudflare.RateLimit{ Threshold: d.Get("threshold").(int), Period: d.Get("period").(int), - Action: expandRateLimitAction(d), + Action: rateLimitAction, + } + + newRateLimitAction, err := expandRateLimitAction(d) + if err != nil { + return err } + updatedRateLimit.Action = newRateLimitAction newRateLimitMatch, err := expandRateLimitTrafficMatcher(d) if err != nil { @@ -331,6 +353,7 @@ func expandRateLimitTrafficMatcher(d *schema.ResourceData) (matcher cloudflare.R } responseMatcher.Statuses = statuses } + if originIface, ok := matchResp["origin_traffic"]; ok { originTraffic := originIface.(bool) responseMatcher.OriginTraffic = &originTraffic @@ -340,15 +363,24 @@ func expandRateLimitTrafficMatcher(d *schema.ResourceData) (matcher cloudflare.R return } -func expandRateLimitAction(d *schema.ResourceData) cloudflare.RateLimitAction { +func expandRateLimitAction(d *schema.ResourceData) (action cloudflare.RateLimitAction, err error) { // dont need to guard for array length because MinItems is set **and** action is required tfAction := d.Get("action").([]interface{})[0].(map[string]interface{}) - action := cloudflare.RateLimitAction{ - Mode: tfAction["mode"].(string), - Timeout: tfAction["timeout"].(int), + mode := tfAction["mode"].(string) + timeout := tfAction["timeout"].(int) + + if timeout == 0 { + if mode == "simulate" || mode == "ban" { + return action, fmt.Errorf("rate limit timeout must be set if the 'mode' is simulate or ban") + } + } else if mode == "challenge" || mode == "js_challenge" { + return action, fmt.Errorf("rate limit timeout must not be set if the 'mode' is challenge or js_challenge") } + action.Mode = mode + action.Timeout = timeout + if _, ok := tfAction["response"]; ok && len(tfAction["response"].([]interface{})) > 0 { log.Printf("[DEBUG] Cloudflare Rate Limit specified action: %+v \n", tfAction) tfActionResponse := tfAction["response"].([]interface{})[0].(map[string]interface{}) @@ -358,7 +390,7 @@ func expandRateLimitAction(d *schema.ResourceData) cloudflare.RateLimitAction { Body: tfActionResponse["body"].(string), } } - return action + return action, nil } func expandRateLimitCorrelate(d *schema.ResourceData) (correlate cloudflare.RateLimitCorrelate, err error) { diff --git a/cloudflare/resource_cloudflare_rate_limit_test.go b/cloudflare/resource_cloudflare_rate_limit_test.go index 90e68fe9dc..dcd5c584fa 100644 --- a/cloudflare/resource_cloudflare_rate_limit_test.go +++ b/cloudflare/resource_cloudflare_rate_limit_test.go @@ -50,6 +50,41 @@ func TestAccCloudflareRateLimit_Basic(t *testing.T) { }) } +func TestAccCloudflareRateLimitChallenge_Basic(t *testing.T) { + t.Parallel() + var rateLimit cloudflare.RateLimit + zone := os.Getenv("CLOUDFLARE_DOMAIN") + rnd := acctest.RandString(10) + name := "cloudflare_rate_limit." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudflareRateLimitDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareRateLimitChallengeConfigBasic(zone, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareRateLimitExists(name, &rateLimit), + testAccCheckCloudflareRateLimitIDIsValid(name, zone), + // check that the action challenge mode has been set + resource.TestCheckResourceAttr(name, "action.0.mode", "challenge"), + resource.TestCheckResourceAttr(name, "action.0.response.#", "0"), + resource.TestCheckResourceAttr(name, "bypass_url_patterns.#", "0"), + resource.TestCheckResourceAttr(name, "match.0.response.0.statuses.#", "0"), + resource.TestCheckResourceAttr(name, "disabled", "false"), + resource.TestCheckResourceAttr(name, "match.#", "1"), + resource.TestCheckResourceAttr(name, "match.0.request.#", "1"), + resource.TestCheckResourceAttr(name, "match.0.request.0.schemes.#", "1"), + resource.TestCheckResourceAttr(name, "match.0.request.0.url_pattern", "*"), + resource.TestCheckResourceAttr(name, "match.0.response.#", "1"), + resource.TestCheckResourceAttr(name, "match.0.response.0.origin_traffic", "true"), + ), + }, + }, + }) +} + func TestAccCloudflareRateLimit_FullySpecified(t *testing.T) { t.Parallel() var rateLimit cloudflare.RateLimit @@ -167,6 +202,42 @@ func TestAccCloudflareRateLimit_CreateAfterManualDestroy(t *testing.T) { }) } +func TestAccCloudflareRateLimit_WithoutTimeout(t *testing.T) { + t.Parallel() + zone := os.Getenv("CLOUDFLARE_DOMAIN") + rnd := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudflareRateLimitDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareRateLimitConfigWithoutTimeout(zone, rnd), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("rate limit timeout must be set if the 'mode' is simulate or ban")), + }, + }, + }) +} + +func TestAccCloudflareRateLimit_ChallengeWithTimeout(t *testing.T) { + t.Parallel() + zone := os.Getenv("CLOUDFLARE_DOMAIN") + rnd := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudflareRateLimitDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareRateLimitChallengeConfigWithTimeout(zone, rnd), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("rate limit timeout must not be set if the 'mode' is challenge or js_challenge")), + }, + }, + }) +} + func testAccCheckCloudflareRateLimitDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*cloudflare.API) @@ -255,7 +326,7 @@ func testAccCheckCloudflareRateLimitConfigBasic(zone, id string) string { resource "cloudflare_rate_limit" "%[1]s" { zone = "%[2]s" threshold = 1000 - period = 1 + period = 10 action { mode = "simulate" timeout = 86400 @@ -268,7 +339,7 @@ func testAccCheckCloudflareRateLimitConfigMatchingUrl(zone, id string) string { resource "cloudflare_rate_limit" "%[1]s" { zone = "%[2]s" threshold = 1000 - period = 1 + period = 10 match { request { url_pattern = "%[2]s/tfacc-url-%[1]s" @@ -286,7 +357,7 @@ func testAccCheckCloudflareRateLimitConfigFullySpecified(zone, id string) string resource "cloudflare_rate_limit" "%[1]s" { zone = "%[2]s" threshold = 2000 - period = 2 + period = 10 match { request { url_pattern = "%[2]s/tfacc-full-%[1]s" @@ -314,3 +385,40 @@ resource "cloudflare_rate_limit" "%[1]s" { bypass_url_patterns = ["%[2]s/bypass1","%[2]s/bypass2"] }`, id, zone) } + +func testAccCheckCloudflareRateLimitChallengeConfigBasic(zone, id string) string { + return fmt.Sprintf(` +resource "cloudflare_rate_limit" "%[1]s" { + zone = "%[2]s" + threshold = 1000 + period = 10 + action { + mode = "challenge" + } +}`, id, zone) +} + +func testAccCheckCloudflareRateLimitConfigWithoutTimeout(zone, id string) string { + return fmt.Sprintf(` +resource "cloudflare_rate_limit" "%[1]s" { + zone = "%[2]s" + threshold = 1000 + period = 10 + action { + mode = "simulate" + } +}`, id, zone) +} + +func testAccCheckCloudflareRateLimitChallengeConfigWithTimeout(zone, id string) string { + return fmt.Sprintf(` +resource "cloudflare_rate_limit" "%[1]s" { + zone = "%[2]s" + threshold = 1000 + period = 10 + action { + mode = "challenge" + timeout = 60 + } +}`, id, zone) +} diff --git a/website/docs/r/rate_limit.html.markdown b/website/docs/r/rate_limit.html.markdown index 72b627827e..9250a498c9 100644 --- a/website/docs/r/rate_limit.html.markdown +++ b/website/docs/r/rate_limit.html.markdown @@ -78,7 +78,7 @@ The **match.response** block supports: The **action** block supports: * `mode` - (Required) The type of action to perform. Allowable values are 'simulate' and 'ban'. -* `timeout` - (Required) The time in seconds as an integer to perform the mitigation action. Must be the same or greater than the period (min: 1, max: 86400). +* `timeout` - (Optional) The time in seconds as an integer to perform the mitigation action. This field is required if the `mode` is either `simulate` or `ban`. Must be the same or greater than the period (min: 1, max: 86400). * `response` - (Optional) Custom content-type and body to return, this overrides the custom error for the zone. This field is not required. Omission will result in default HTML error page. Definition below. The **action.response** block supports: