Skip to content

Commit

Permalink
Add rate limit options for compute resource security policy rules (Go…
Browse files Browse the repository at this point in the history
…ogleCloudPlatform#5413)

Co-authored-by: Riley Karson <[email protected]>
  • Loading branch information
2 people authored and lcaggio committed Mar 16, 2022
1 parent 4f88894 commit e906b95
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func resourceComputeSecurityPolicy() *schema.Resource {
"action": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"allow", "deny(403)", "deny(404)", "deny(502)"}, false),
ValidateFunc: validation.StringInSlice([]string{"allow", "deny(403)", "deny(404)", "deny(502)", "rate_based_ban", "throttle"}, false),
Description: `Action to take when match matches the request. Valid values: "allow" : allow access to target, "deny(status)" : deny access to target, returns the HTTP response code specified (valid values are 403, 404 and 502)`,
},

Expand Down Expand Up @@ -154,6 +154,119 @@ func resourceComputeSecurityPolicy() *schema.Resource {
Computed: true,
Description: `When set to true, the action specified above is not enforced. Stackdriver logs for requests that trigger a preview action are annotated as such.`,
},

<% unless version == 'ga' -%>
"rate_limit_options": {
Type: schema.TypeList,
Optional: true,
Description: `Rate limit threshold for this security policy. Must be specified if the action is "rate_based_ban" or "throttle". Cannot be specified for any other actions.`,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"rate_limit_threshold": {
Type: schema.TypeList,
Required: true,
Description: `Threshold at which to begin ratelimiting.`,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"count": {
Type: schema.TypeInt,
Required: true,
Description: `Number of HTTP(S) requests for calculating the threshold.`,
},

"interval_sec": {
Type: schema.TypeInt,
Required: true,
Description: `Interval over which the threshold is computed.`,
},
},
},
},

"conform_action": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"allow"}, false),
Description: `Action to take for requests that are under the configured rate limit threshold. Valid option is "allow" only.`,
},

"exceed_action": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"redirect", "deny(403)", "deny(404)", "deny(429)", "deny(502)"}, false),
Description: `Action to take for requests that are above the configured rate limit threshold, to either deny with a specified HTTP response code, or redirect to a different endpoint. Valid options are "deny()" where valid values for status are 403, 404, 429, and 502, and "redirect" where the redirect parameters come from exceedRedirectOptions below.`,
},

"enforce_on_key": {
Type: schema.TypeString,
Optional: true,
Default: "ALL",
ValidateFunc: validation.StringInSlice([]string{"ALL", "IP", "HTTP_HEADER", "XFF_IP"}, false),
Description: `Determines the key to enforce the rateLimitThreshold on. Possible values are: "ALL" -- A single rate limit threshold is applied to all the requests matching this rule. This is the default value if this field 'enforceOnKey' is not configured. "IP" -- The source IP address of the request is the key. Each IP has this limit enforced separately. "HTTP_HEADER" -- The value of the HTTP Header whose name is configured under "enforceOnKeyName". The key value is truncated to the first 128 bytes of the Header value. If no such header is present in the request, the key type defaults to "ALL". "XFF_IP" -- The first IP address (i.e. the originating client IP address) specified in the list of IPs under X-Forwarded-For HTTP Header. If no such header is present or the value is not a valid IP, the key type defaults to "ALL".`,
},

"enforce_on_key_name": {
Type: schema.TypeString,
Optional: true,
Description: `Rate limit key name applicable only for the following key types: HTTP_HEADER -- Name of the HTTP Header whose value is taken as the key value.`,
},

"ban_threshold": {
Type: schema.TypeList,
Optional: true,
Description: `Can only be specified if the action for the rule is "rate_based_ban". If specified, the key will be banned for the configured 'banDurationSec' when the number of requests that exceed the 'rateLimitThreshold' also exceed this 'banThreshold'.`,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"count": {
Type: schema.TypeInt,
Required: true,
Description: `Number of HTTP(S) requests for calculating the threshold.`,
},

"interval_sec": {
Type: schema.TypeInt,
Required: true,
Description: `Interval over which the threshold is computed.`,
},
},
},
},

"ban_duration_sec": {
Type: schema.TypeInt,
Optional: true,
Description: `Can only be specified if the action for the rule is "rate_based_ban". If specified, determines the time (in seconds) the traffic will continue to be banned by the rate limit after the rate falls below the threshold.`,
},

"exceed_redirect_options": {
Type: schema.TypeList,
Optional: true,
Description: `Parameters defining the redirect action that is used as the exceed action. Cannot be specified if the exceed action is not redirect.`,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
Required: true,
Description: `Type of the redirect action.`,
ValidateFunc: validation.StringInSlice([]string{"EXTERNAL_302", "GOOGLE_RECAPTCHA"}, false),
},

"target": {
Type: schema.TypeString,
Optional: true,
Description: `Target for the redirect action. This is required if the type is EXTERNAL_302 and cannot be specified for GOOGLE_RECAPTCHA.`,
},
},
},
},
},
},
},
<% end -%>
},
},
Description: `The set of rules that belong to this policy. There must always be a default rule (rule with priority 2147483647 and match "*"). If no rules are provided when creating a security policy, a default rule with action "allow" will be added.`,
Expand Down Expand Up @@ -497,12 +610,15 @@ func expandSecurityPolicyRules(configured []interface{}) []*compute.SecurityPoli
func expandSecurityPolicyRule(raw interface{}) *compute.SecurityPolicyRule {
data := raw.(map[string]interface{})
return &compute.SecurityPolicyRule{
Description: data["description"].(string),
Priority: int64(data["priority"].(int)),
Action: data["action"].(string),
Preview: data["preview"].(bool),
Match: expandSecurityPolicyMatch(data["match"].([]interface{})),
ForceSendFields: []string{"Description", "Preview"},
Description: data["description"].(string),
Priority: int64(data["priority"].(int)),
Action: data["action"].(string),
Preview: data["preview"].(bool),
Match: expandSecurityPolicyMatch(data["match"].([]interface{})),
<% unless version == 'ga' -%>
RateLimitOptions: expandSecurityPolicyRuleRateLimitOptions(data["rate_limit_options"].([]interface{})),
<% end -%>
ForceSendFields: []string{"Description", "Preview"},
}
}

Expand Down Expand Up @@ -549,11 +665,14 @@ func flattenSecurityPolicyRules(rules []*compute.SecurityPolicyRule) []map[strin
rulesSchema := make([]map[string]interface{}, 0, len(rules))
for _, rule := range rules {
data := map[string]interface{}{
"description": rule.Description,
"priority": rule.Priority,
"action": rule.Action,
"preview": rule.Preview,
"match": flattenMatch(rule.Match),
"description": rule.Description,
"priority": rule.Priority,
"action": rule.Action,
"preview": rule.Preview,
"match": flattenMatch(rule.Match),
<% unless version == 'ga' -%>
"rate_limit_options": flattenSecurityPolicyRuleRateLimitOptions(rule.RateLimitOptions),
<% end -%>
}

rulesSchema = append(rulesSchema, data)
Expand Down Expand Up @@ -653,6 +772,68 @@ func flattenLayer7DdosDefenseConfig(conf *compute.SecurityPolicyAdaptiveProtecti
}
<% end -%>

<% unless version == 'ga' -%>
func expandSecurityPolicyRuleRateLimitOptions(configured []interface{}) *compute.SecurityPolicyRuleRateLimitOptions {
if len(configured) == 0 || configured[0] == nil {
return nil
}

data := configured[0].(map[string]interface{})
return &compute.SecurityPolicyRuleRateLimitOptions{
BanThreshold: expandThreshold(data["ban_threshold"].([]interface{})),
RateLimitThreshold: expandThreshold(data["rate_limit_threshold"].([]interface{})),
ExceedAction: data["exceed_action"].(string),
ConformAction: data["conform_action"].(string),
EnforceOnKey: data["enforce_on_key"].(string),
EnforceOnKeyName: data["enforce_on_key_name"].(string),
BanDurationSec: int64(data["ban_duration_sec"].(int)),
}
}

func expandThreshold(configured []interface{}) *compute.SecurityPolicyRuleRateLimitOptionsThreshold {
if len(configured) == 0 || configured[0] == nil {
return nil
}

data := configured[0].(map[string]interface{})
return &compute.SecurityPolicyRuleRateLimitOptionsThreshold{
Count: int64(data["count"].(int)),
IntervalSec: int64(data["interval_sec"].(int)),
}
}

func flattenSecurityPolicyRuleRateLimitOptions(conf *compute.SecurityPolicyRuleRateLimitOptions) []map[string]interface{} {
if conf == nil {
return nil
}

data := map[string]interface{}{
"ban_threshold": flattenThreshold(conf.BanThreshold),
"rate_limit_threshold": flattenThreshold(conf.RateLimitThreshold),
"exceed_action": conf.ExceedAction,
"conform_action": conf.ConformAction,
"enforce_on_key": conf.EnforceOnKey,
"enforce_on_key_name": conf.EnforceOnKeyName,
"ban_duration_sec": conf.BanDurationSec,
}

return []map[string]interface{}{data}
}

func flattenThreshold(conf *compute.SecurityPolicyRuleRateLimitOptionsThreshold) []map[string]interface{} {
if conf == nil {
return nil
}

data := map[string]interface{}{
"count": conf.Count,
"interval_sec": conf.IntervalSec,
}

return []map[string]interface{}{data}
}
<% end -%>

func resourceSecurityPolicyStateImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
config := meta.(*Config)
if err := parseImportId([]string{"projects/(?P<project>[^/]+)/global/securityPolicies/(?P<name>[^/]+)", "(?P<project>[^/]+)/(?P<name>[^/]+)", "(?P<name>[^/]+)"}, d, config); err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ func TestAccComputeSecurityPolicy_withAdaptiveProtection(t *testing.T) {
}
<% end -%>

<% unless version == 'ga' -%>
func TestAccComputeSecurityPolicy_withRateLimitOptions(t *testing.T) {
t.Parallel()

spName := fmt.Sprintf("tf-test-%s", randString(t, 10))

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeSecurityPolicyDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeSecurityPolicy_withRateLimitOptions(spName),
},
{
ResourceName: "google_compute_security_policy.policy",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
<% end -%>

func testAccCheckComputeSecurityPolicyDestroyProducer(t *testing.T) func(s *terraform.State) error {
return func(s *terraform.State) error {
config := googleProviderConfig(t)
Expand Down Expand Up @@ -356,3 +380,48 @@ resource "google_compute_security_policy" "policy" {
`, spName)
}
<% end -%>

<% unless version == 'ga' -%>
func testAccComputeSecurityPolicy_withRateLimitOptions(spName string) string {
return fmt.Sprintf(`
resource "google_compute_security_policy" "policy" {
name = "%s"
description = "updated description"

rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}

rule {
action = "throttle"
priority = 100
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = [
"0.0.0.0/32",
]
}
}
rate_limit_options {
conform_action = "allow"
exceed_action = "deny(403)"
enforce_on_key = "IP"
rate_limit_threshold {
count = 100
interval_sec = 60
}
}
}
}
`, spName)
}
<% end -%>
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ The following arguments are supported:
* `action` - (Required) Action to take when `match` matches the request. Valid values:
* "allow" : allow access to target
* "deny(status)" : deny access to target, returns the HTTP response code specified (valid values are 403, 404 and 502)
* "rate_based_ban" : limit client traffic to the configured threshold and ban the client if the traffic exceeds the threshold. Configure parameters for this action in RateLimitOptions. Requires rateLimitOptions to be set.
* "threshold" : limit client traffic to the configured threshold. Configure parameters for this action in rateLimitOptions. Requires rateLimitOptions to be set for this.

* `priority` - (Required) An unique positive integer indicating the priority of evaluation for a rule.
Rules are evaluated from highest priority (lowest numerically) to lowest priority (highest numerically) in order.
Expand All @@ -83,6 +85,9 @@ The following arguments are supported:
* `preview` - (Optional) When set to true, the `action` specified above is not enforced.
Stackdriver logs for requests that trigger a preview action are annotated as such.

* `rate_limit_options` - (Optional, [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html))
Must be specified if the `action` is "rate_based_bad" or "throttle". Cannot be specified for other actions. Structure is [documented below](#nested_rate_limit_options).

<a name="nested_match"></a>The `match` block supports:

* `config` - (Optional) The configuration options available when specifying `versioned_expr`.
Expand All @@ -108,6 +113,33 @@ The following arguments are supported:
* `expression` - (Required) Textual representation of an expression in Common Expression Language syntax.
The application context of the containing message determines which well-known feature set of CEL is supported.

<a name="nested_rate_limit_options"></a>The `rate_limit_options` block supports:

* `ban_duration_sec` - (Optional) Can only be specified if the `action` for the rule is "rate_based_ban".
If specified, determines the time (in seconds) the traffic will continue to be banned by the rate limit after the rate falls below the threshold.

* `ban_threshold` - (Optional) Can only be specified if the `action` for the rule is "rate_based_ban".
If specified, the key will be banned for the configured 'ban_duration_sec' when the number of requests that exceed the 'rate_limit_threshold' also
exceed this 'ban_threshold'. Structure is [documented below](#nested_threshold).

* `conform_action` - (Optional) Action to take for requests that are under the configured rate limit threshold. Valid option is "allow" only.

* `enforce_on_key` - (Optional) Determines the key to enforce the rate_limit_threshold on.
Possible values incude "ALL", "ALL_IPS", "HTTP_HEADER", "IP", "XFF_IP". If not specified, defaults to "ALL".

* `enforce_on_key_name` - (Optional) Rate limit key name applicable only for HTTP_HEADER key types. Name of the HTTP header whose value is taken as the key value.

* `exceed_action` - (Optional) When a request is denied, returns the HTTP response code specified.
Valid options are "deny()" where valid values for status are 403, 404, 429, and 502.

* `rate_limit_threshold` - (Optional) Threshold at which to begin ratelimiting. Structure is [documented below](#nested_threshold).

<a name="nested_threshold"></a>The `{ban/rate_limit}_threshold` block supports:

* `count` - (Optional) Number of HTTP(S) requests for calculating the threshold.

* `interval_sec` - (Optional) Interval over which the threshold is computed.

<a name="nested_adaptive_protection_config"></a>The `adaptive_protection_config` block supports:

* `layer_7_ddos_defense_config` - (Optional, [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html)) Configuration for [Google Cloud Armor Adaptive Protection Layer 7 DDoS Defense](https://cloud.google.com/armor/docs/adaptive-protection-overview?hl=en). Structure is [documented below](#nested_layer_7_ddos_defense_config).
Expand Down

0 comments on commit e906b95

Please sign in to comment.