Skip to content

Commit

Permalink
Add challenge and js_challenge rate-limit modes (#172)
Browse files Browse the repository at this point in the history
* Add challenge and js_challenge rate-limit modes
* Attempt to raise errors on incorrectly defined ratelimit
  • Loading branch information
jdarley authored and patryk committed Jan 7, 2019
1 parent a90979b commit 340950a
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 13 deletions.
50 changes: 41 additions & 9 deletions cloudflare/resource_cloudflare_rate_limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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{})
Expand All @@ -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) {
Expand Down
114 changes: 111 additions & 3 deletions cloudflare/resource_cloudflare_rate_limit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion website/docs/r/rate_limit.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 340950a

Please sign in to comment.