diff --git a/client/firewall_config.go b/client/firewall_config.go
new file mode 100644
index 00000000..5b7ee8c2
--- /dev/null
+++ b/client/firewall_config.go
@@ -0,0 +1,120 @@
+package client
+
+import (
+ "context"
+ "fmt"
+)
+
+type FirewallConfig struct {
+ ProjectID string `json:"-"`
+ TeamID string `json:"-"`
+ Enabled bool `json:"firewallEnabled"`
+ ManagedRulesets map[string]ManagedRule `json:"managedRules,omitempty"`
+
+ Rules []FirewallRule `json:"rules,omitempty"`
+ IPRules []IPRule `json:"ips,omitempty"`
+ CRS map[string]CoreRuleSet `json:"crs,omitempty"`
+}
+type ManagedRule struct {
+ Active bool `json:"active"`
+}
+
+type FirewallRule struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Active bool `json:"active"`
+ ConditionGroup []ConditionGroup `json:"conditionGroup"`
+ Action Action `json:"action"`
+}
+
+type ConditionGroup struct {
+ Conditions []Condition `json:"conditions"`
+}
+
+type Condition struct {
+ Type string `json:"type"`
+ Op string `json:"op"`
+ Neg bool `json:"neg"`
+ Key string `json:"key"`
+ Value string `json:"value"`
+}
+
+type Action struct {
+ Mitigate Mitigate `json:"mitigate"`
+}
+type Mitigate struct {
+ Action string `json:"action"`
+ RateLimit *RateLimit `json:"rateLimit,omitempty"`
+ Redirect *Redirect `json:"redirect,omitempty"`
+ ActionDuration string `json:"actionDuration,omitempty"`
+}
+
+type RateLimit struct {
+ Algo string `json:"algo" tfsdk:"algo"`
+ Window int64 `json:"window" tfsdk:"window"`
+ Limit int64 `json:"limit" tfsdk:"limit"`
+ Keys []string `json:"keys" tfsdk:"keys"`
+ Action string `json:"action" tfsdk:"action"`
+}
+
+type Redirect struct {
+ Location string `json:"location" tfsdk:"location"`
+ Permanent bool `json:"permanent" tfsdk:"permanent"`
+}
+
+type IPRule struct {
+ ID string `json:"id,omitempty"`
+ Hostname string `json:"hostname"`
+ IP string `json:"ip"`
+ Notes string `json:"notes,omitempty"`
+ Action string `json:"action"`
+}
+
+type CoreRuleSet struct {
+ Active bool `json:"active"`
+ Action string `json:"action"`
+}
+
+func (c *Client) GetFirewallConfig(ctx context.Context, projectId string, teamId string) (FirewallConfig, error) {
+ teamId = c.teamID(teamId)
+ url := fmt.Sprintf(
+ "%s/v1/security/firewall/config/active?projectId=%s&teamId=%s",
+ c.baseURL,
+ projectId,
+ teamId,
+ )
+ var res = FirewallConfig{}
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "GET",
+ url: url,
+ }, &res)
+ res.TeamID = teamId
+ return res, err
+}
+
+func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (FirewallConfig, error) {
+ teamId := c.teamID(cfg.TeamID)
+ url := fmt.Sprintf(
+ "%s/v1/security/firewall/config?projectId=%s&teamId=%s",
+ c.baseURL,
+ cfg.ProjectID,
+ teamId,
+ )
+
+ var res struct {
+ Active FirewallConfig `json:"active"`
+ Error map[string]string `json:"error,omitempty"`
+ }
+ payload := mustMarshal(cfg)
+
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "PUT",
+ url: url,
+ body: string(payload),
+ }, &res)
+ res.Active.TeamID = teamId
+ return res.Active, err
+}
diff --git a/docs/resources/attack_challenge_mode.md b/docs/resources/attack_challenge_mode.md
index c82cd74d..2bccb5b9 100644
--- a/docs/resources/attack_challenge_mode.md
+++ b/docs/resources/attack_challenge_mode.md
@@ -32,7 +32,7 @@ resource "vercel_attack_challenge_mode" "example" {
### Required
- `enabled` (Boolean) Whether Attack Challenge Mode is enabled or not.
-- `project_id` (String) The ID of the Project to adjust the CPU for.
+- `project_id` (String) The ID of the Project to toggle Attack Challenge Mode on.
### Optional
diff --git a/docs/resources/firewall_config.md b/docs/resources/firewall_config.md
new file mode 100644
index 00000000..cdb0d39d
--- /dev/null
+++ b/docs/resources/firewall_config.md
@@ -0,0 +1,432 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "vercel_firewall_config Resource - terraform-provider-vercel"
+subcategory: ""
+description: |-
+ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Network.
+---
+
+# vercel_firewall_config (Resource)
+
+Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Network.
+
+## Example Usage
+
+```terraform
+resource "vercel_project" "example" {
+ name = "firewall-config-example"
+}
+
+resource "vercel_firewall_config" "example" {
+ project_id = vercel_project.example.id
+
+ rules {
+ rule {
+ name = "Bypass Known request"
+ description = "Bypass requests using internal bearer tokens"
+ # individual condition groups are evaluated as ORs
+ condition_group = [
+ {
+ conditions = [{
+ type = "header"
+ key = "Authorization"
+ op = "eq"
+ value = "Bearer internaltoken"
+ }]
+ },
+ {
+ conditions = [{
+ type = "header"
+ key = "Authorization"
+ op = "eq"
+ value = "Bearer internaltoken2"
+ }]
+ }
+ ]
+ action = {
+ action = "bypass"
+ }
+ }
+
+ rule {
+ name = "Challenge curl"
+ description = "Challenge user agents containing 'curl'"
+ condition_group = [{
+ conditions = [{
+ type = "user_agent"
+ op = "sub"
+ value = "curl"
+ }]
+ }]
+ action = {
+ action = "challenge"
+ }
+ }
+
+ rule {
+ name = "Deny cookieless requests"
+ description = "requests to /api that are missing a session cookie"
+ # multiple conditions in a single condition group are evaluated as ANDs
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ },
+ {
+ type = "cookie"
+ key = "_session"
+ neg = true
+ op = "ex"
+ }]
+ }]
+ action = {
+ action = "challenge"
+ }
+ }
+
+ rule {
+ name = "Rate limit API"
+ description = "apply ratelimit to requests under /api"
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "pre"
+ value = "/api"
+ }]
+ }]
+
+ action = {
+ action = "rate_limit"
+ rate_limit = {
+ limit = 100
+ window = 300
+ keys = ["ip", "ja4"]
+ algo = "fixed_window"
+ action = "deny"
+ }
+ actionDuration = "5m"
+ }
+ }
+ }
+}
+
+resource "vercel_project" "managed_example" {
+ name = "firewall-managed-rule-example"
+}
+
+resource "vercel_firewall_config" "managed" {
+ project_id = vercel_project.managed.id
+
+ managed_rulesets {
+ owasp {
+ xss = { action = "deny" }
+ sqli = { action = "deny" }
+ rce = { action = "deny" }
+ php = { action = "deny" }
+ java = { action = "deny" }
+ lfi = { action = "deny" }
+ rfi = { action = "deny" }
+ gen = { action = "deny" }
+ }
+ }
+}
+
+resource "vercel_project" "ip_example" {
+ name = "firewall-ip-blocking-example"
+}
+
+resource "vercel_firewall_config" "ip-blocking" {
+ project_id = vercel_project.ip_example.id
+
+ ip_rules {
+ # deny this subnet for all my hosts
+ rule {
+ action = "deny"
+ ip = "51.85.0.0/16"
+ hostname ="*"
+ }
+
+ rule {
+ action = "challenge"
+ ip = "1.2.3.4"
+ hostname = "example.com"
+ }
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `project_id` (String) The ID of the project this configuration belongs to.
+
+### Optional
+
+- `enabled` (Boolean) Whether firewall is enabled or not.
+- `ip_rules` (Block, Optional) IP rules to apply to the project. (see [below for nested schema](#nestedblock--ip_rules))
+- `managed_rulesets` (Block, Optional) The managed rulesets that are enabled. (see [below for nested schema](#nestedblock--managed_rulesets))
+- `rules` (Block, Optional) Custom rules to apply to the project (see [below for nested schema](#nestedblock--rules))
+- `team_id` (String) The ID of the team this project belongs to.
+
+
+### Nested Schema for `ip_rules`
+
+Optional:
+
+- `rule` (Block List) (see [below for nested schema](#nestedblock--ip_rules--rule))
+
+
+### Nested Schema for `ip_rules.rule`
+
+Required:
+
+- `action` (String)
+- `hostname` (String) Hosts to apply these rules to
+- `ip` (String) IP or CIDR to block
+
+Optional:
+
+- `notes` (String)
+
+Read-Only:
+
+- `id` (String) The ID of this resource.
+
+
+
+
+### Nested Schema for `managed_rulesets`
+
+Optional:
+
+- `owasp` (Block, Optional) Enable the owasp managed rulesets and select ruleset behaviors (see [below for nested schema](#nestedblock--managed_rulesets--owasp))
+
+
+### Nested Schema for `managed_rulesets.owasp`
+
+Optional:
+
+- `gen` (Attributes) Generic Attack Detection (see [below for nested schema](#nestedatt--managed_rulesets--owasp--gen))
+- `java` (Attributes) Java Attack Detection (see [below for nested schema](#nestedatt--managed_rulesets--owasp--java))
+- `lfi` (Attributes) Local File Inclusion Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--lfi))
+- `ma` (Attributes) Multipart Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--ma))
+- `php` (Attributes) PHP Attack Detection (see [below for nested schema](#nestedatt--managed_rulesets--owasp--php))
+- `rce` (Attributes) Remote Code Execution Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rce))
+- `rfi` (Attributes) Remote File Inclusion Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rfi))
+- `sd` (Attributes) Scanner Detection Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sd))
+- `sqli` (Attributes) SQL Injection Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sqli))
+- `xss` (Attributes) Cross Site Scripting Rules (see [below for nested schema](#nestedatt--managed_rulesets--owasp--xss))
+
+
+### Nested Schema for `managed_rulesets.owasp.gen`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.java`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.lfi`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.ma`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.php`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.rce`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.rfi`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.sd`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.sqli`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+### Nested Schema for `managed_rulesets.owasp.xss`
+
+Required:
+
+- `action` (String)
+
+Optional:
+
+- `active` (Boolean)
+
+
+
+
+
+### Nested Schema for `rules`
+
+Optional:
+
+- `rule` (Block List) (see [below for nested schema](#nestedblock--rules--rule))
+
+
+### Nested Schema for `rules.rule`
+
+Required:
+
+- `action` (Attributes) Actions to take when the condition groups match a request (see [below for nested schema](#nestedatt--rules--rule--action))
+- `condition_group` (Attributes List) Sets of conditions that may match a request (see [below for nested schema](#nestedatt--rules--rule--condition_group))
+- `name` (String) Name to identify the rule
+
+Optional:
+
+- `active` (Boolean) Rule is active or disabled
+- `description` (String)
+
+Read-Only:
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `rules.rule.action`
+
+Required:
+
+- `action` (String) Base action
+
+Optional:
+
+- `action_duration` (String) Forward persistence of a rule aciton
+- `rate_limit` (Attributes) Behavior or a rate limiting action. Required if action is rate_limit (see [below for nested schema](#nestedatt--rules--rule--action--rate_limit))
+- `redirect` (Attributes) How to redirect a request. Required if action is redirect (see [below for nested schema](#nestedatt--rules--rule--action--redirect))
+
+
+### Nested Schema for `rules.rule.action.rate_limit`
+
+Required:
+
+- `action` (String) Action to take when rate limit is exceeded
+- `algo` (String) Rate limiting algorithm
+- `keys` (List of String) Keys used to bucket an individual client
+- `limit` (Number) number of requests allowed in the window
+- `window` (Number) Time window in seconds
+
+
+
+### Nested Schema for `rules.rule.action.redirect`
+
+Required:
+
+- `location` (String)
+- `permanent` (Boolean)
+
+
+
+
+### Nested Schema for `rules.rule.condition_group`
+
+Required:
+
+- `conditions` (Attributes List) Conditions that must all match within a group (see [below for nested schema](#nestedatt--rules--rule--condition_group--conditions))
+
+
+### Nested Schema for `rules.rule.condition_group.conditions`
+
+Required:
+
+- `op` (String) How to comparse type to value
+- `type` (String) Request key type to match against
+
+Optional:
+
+- `key` (String) Key within type to match against
+- `neg` (Boolean)
+- `value` (String)
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import vercel_firewall_config.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
diff --git a/examples/resources/vercel_firewall_config/import.sh b/examples/resources/vercel_firewall_config/import.sh
new file mode 100644
index 00000000..0f6e3e3a
--- /dev/null
+++ b/examples/resources/vercel_firewall_config/import.sh
@@ -0,0 +1 @@
+terraform import vercel_firewall_config.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/examples/resources/vercel_firewall_config/resource.tf b/examples/resources/vercel_firewall_config/resource.tf
new file mode 100644
index 00000000..f4cf090a
--- /dev/null
+++ b/examples/resources/vercel_firewall_config/resource.tf
@@ -0,0 +1,141 @@
+resource "vercel_project" "example" {
+ name = "firewall-config-example"
+}
+
+resource "vercel_firewall_config" "example" {
+ project_id = vercel_project.example.id
+
+ rules {
+ rule {
+ name = "Bypass Known request"
+ description = "Bypass requests using internal bearer tokens"
+ # individual condition groups are evaluated as ORs
+ condition_group = [
+ {
+ conditions = [{
+ type = "header"
+ key = "Authorization"
+ op = "eq"
+ value = "Bearer internaltoken"
+ }]
+ },
+ {
+ conditions = [{
+ type = "header"
+ key = "Authorization"
+ op = "eq"
+ value = "Bearer internaltoken2"
+ }]
+ }
+ ]
+ action = {
+ action = "bypass"
+ }
+ }
+
+ rule {
+ name = "Challenge curl"
+ description = "Challenge user agents containing 'curl'"
+ condition_group = [{
+ conditions = [{
+ type = "user_agent"
+ op = "sub"
+ value = "curl"
+ }]
+ }]
+ action = {
+ action = "challenge"
+ }
+ }
+
+ rule {
+ name = "Deny cookieless requests"
+ description = "requests to /api that are missing a session cookie"
+ # multiple conditions in a single condition group are evaluated as ANDs
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ },
+ {
+ type = "cookie"
+ key = "_session"
+ neg = true
+ op = "ex"
+ }]
+ }]
+ action = {
+ action = "challenge"
+ }
+ }
+
+ rule {
+ name = "Rate limit API"
+ description = "apply ratelimit to requests under /api"
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "pre"
+ value = "/api"
+ }]
+ }]
+
+ action = {
+ action = "rate_limit"
+ rate_limit = {
+ limit = 100
+ window = 300
+ keys = ["ip", "ja4"]
+ algo = "fixed_window"
+ action = "deny"
+ }
+ actionDuration = "5m"
+ }
+ }
+ }
+}
+
+resource "vercel_project" "managed_example" {
+ name = "firewall-managed-rule-example"
+}
+
+resource "vercel_firewall_config" "managed" {
+ project_id = vercel_project.managed.id
+
+ managed_rulesets {
+ owasp {
+ xss = { action = "deny" }
+ sqli = { action = "deny" }
+ rce = { action = "deny" }
+ php = { action = "deny" }
+ java = { action = "deny" }
+ lfi = { action = "deny" }
+ rfi = { action = "deny" }
+ gen = { action = "deny" }
+ }
+ }
+}
+
+resource "vercel_project" "ip_example" {
+ name = "firewall-ip-blocking-example"
+}
+
+resource "vercel_firewall_config" "ip-blocking" {
+ project_id = vercel_project.ip_example.id
+
+ ip_rules {
+ # deny this subnet for all my hosts
+ rule {
+ action = "deny"
+ ip = "51.85.0.0/16"
+ hostname ="*"
+ }
+
+ rule {
+ action = "challenge"
+ ip = "1.2.3.4"
+ hostname = "example.com"
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index d61abc30..5185ae17 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,9 @@ module github.com/vercel/terraform-provider-vercel
go 1.21
require (
- github.com/hashicorp/terraform-plugin-framework v1.7.0
- github.com/hashicorp/terraform-plugin-go v0.22.1
+ github.com/hashicorp/terraform-plugin-framework v1.10.0
+ github.com/hashicorp/terraform-plugin-framework-validators v0.13.0
+ github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.7.0
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
@@ -57,5 +58,5 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/grpc v1.63.2 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
+ google.golang.org/protobuf v1.34.0 // indirect
)
diff --git a/go.sum b/go.sum
index 965aa7fc..5c6922b1 100644
--- a/go.sum
+++ b/go.sum
@@ -73,10 +73,12 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J
github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw=
github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U=
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
-github.com/hashicorp/terraform-plugin-framework v1.7.0 h1:wOULbVmfONnJo9iq7/q+iBOBJul5vRovaYJIu2cY/Pw=
-github.com/hashicorp/terraform-plugin-framework v1.7.0/go.mod h1:jY9Id+3KbZ17OMpulgnWLSfwxNVYSoYBQFTgsx044CI=
-github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w=
-github.com/hashicorp/terraform-plugin-go v0.22.1/go.mod h1:qrjnqRghvQ6KnDbB12XeZ4FluclYwptntoWCr9QaXTI=
+github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc=
+github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
+github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
+github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
+github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
+github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8=
@@ -208,8 +210,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/vercel/provider.go b/vercel/provider.go
index bbcb7695..5162fb8d 100644
--- a/vercel/provider.go
+++ b/vercel/provider.go
@@ -57,6 +57,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource
newEdgeConfigResource,
newEdgeConfigSchemaResource,
newEdgeConfigTokenResource,
+ newFirewallConfigResource,
newLogDrainResource,
newProjectDeploymentRetentionResource,
newProjectDomainResource,
diff --git a/vercel/resource_attack_challenge_mode.go b/vercel/resource_attack_challenge_mode.go
index c3a7649a..b5bc0e64 100644
--- a/vercel/resource_attack_challenge_mode.go
+++ b/vercel/resource_attack_challenge_mode.go
@@ -63,7 +63,7 @@ Attack Challenge Mode prevent malicious traffic by showing a verification challe
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"project_id": schema.StringAttribute{
- Description: "The ID of the Project to adjust the CPU for.",
+ Description: "The ID of the Project to toggle Attack Challenge Mode on.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go
new file mode 100644
index 00000000..b0e2c71e
--- /dev/null
+++ b/vercel/resource_firewall_config.go
@@ -0,0 +1,991 @@
+package vercel
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/vercel/terraform-provider-vercel/client"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ resource.Resource = &firewallConfigResource{}
+ _ resource.ResourceWithConfigure = &firewallConfigResource{}
+ _ resource.ResourceWithImportState = &firewallConfigResource{}
+)
+
+func newFirewallConfigResource() resource.Resource { return &firewallConfigResource{} }
+
+type firewallConfigResource struct {
+ client *client.Client
+}
+
+func (r *firewallConfigResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_firewall_config"
+}
+
+func (r *firewallConfigResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: `
+Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Network.`,
+ Blocks: map[string]schema.Block{
+ "managed_rulesets": schema.SingleNestedBlock{
+ Description: "The managed rulesets that are enabled.",
+ Blocks: map[string]schema.Block{
+ "owasp": schema.SingleNestedBlock{
+ Description: "Enable the owasp managed rulesets and select ruleset behaviors",
+ Attributes: map[string]schema.Attribute{
+ "xss": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Cross Site Scripting Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "sqli": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "SQL Injection Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "lfi": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Local File Inclusion Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "rfi": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Remote File Inclusion Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "rce": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Remote Code Execution Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "sd": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Scanner Detection Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "ma": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Multipart Rules",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "php": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "PHP Attack Detection",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "gen": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Generic Attack Detection",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ "java": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Java Attack Detection",
+ Attributes: map[string]schema.Attribute{
+ "active": schema.BoolAttribute{
+ Optional: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "rules": schema.SingleNestedBlock{
+ Description: "Custom rules to apply to the project",
+ Blocks: map[string]schema.Block{
+ "rule": schema.ListNestedBlock{
+ Validators: []validator.List{
+ listvalidator.UniqueValues(),
+ },
+ NestedObject: schema.NestedBlockObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ Description: "Name to identify the rule",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthBetween(4, 160),
+ },
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtMost(260),
+ },
+ },
+ "active": schema.BoolAttribute{
+ Description: "Rule is active or disabled",
+ Optional: true,
+ },
+ "action": schema.SingleNestedAttribute{
+ Description: "Actions to take when the condition groups match a request",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "action": schema.StringAttribute{
+ Description: "Base action",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit", "redirect"),
+ },
+ },
+ "rate_limit": schema.SingleNestedAttribute{
+ Description: "Behavior or a rate limiting action. Required if action is rate_limit",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "algo": schema.StringAttribute{
+ Description: "Rate limiting algorithm",
+ Required: true,
+ },
+ "window": schema.Int64Attribute{
+ Description: "Time window in seconds",
+ Required: true,
+ },
+ "limit": schema.Int64Attribute{
+ Description: "number of requests allowed in the window",
+ Required: true,
+ },
+ "keys": schema.ListAttribute{
+ Description: "Keys used to bucket an individual client",
+ Required: true,
+ ElementType: types.StringType,
+ },
+ "action": schema.StringAttribute{
+ Description: "Action to take when rate limit is exceeded",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit"),
+ },
+ },
+ },
+ },
+ "redirect": schema.SingleNestedAttribute{
+ Description: "How to redirect a request. Required if action is redirect",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "location": schema.StringAttribute{
+ Required: true,
+ },
+ "permanent": schema.BoolAttribute{
+ Required: true,
+ },
+ },
+ },
+ "action_duration": schema.StringAttribute{
+ Description: "Forward persistence of a rule aciton",
+ Optional: true,
+ },
+ },
+ },
+ "condition_group": schema.ListNestedAttribute{
+ Description: "Sets of conditions that may match a request",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "conditions": schema.ListNestedAttribute{
+ Description: "Conditions that must all match within a group",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{
+ Description: "Request key type to match against",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "host",
+ "path",
+ "method",
+ "header",
+ "query",
+ "cookie",
+ "target_path",
+ "ip_address",
+ "region",
+ "protocol",
+ "scheme",
+ "environment",
+ "user_agent",
+ "geo_continent",
+ "geo_country",
+ "geo_country_region",
+ "geo_city",
+ "geo_as_number",
+ "ja4_digest",
+ "ja3_digest",
+ ),
+ },
+ },
+ "op": schema.StringAttribute{
+ Description: "How to comparse type to value",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "re",
+ "eq",
+ "neq",
+ "ex",
+ "nex",
+ "inc",
+ "ninc",
+ "pre",
+ "suf",
+ "sub",
+ "gt",
+ "gte",
+ "lt",
+ "lte",
+ ),
+ },
+ },
+ "neg": schema.BoolAttribute{
+ Optional: true,
+ },
+ "key": schema.StringAttribute{
+ Description: "Key within type to match against",
+ Optional: true,
+ },
+ "value": schema.StringAttribute{
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "ip_rules": schema.SingleNestedBlock{
+ Description: "IP rules to apply to the project.",
+ Blocks: map[string]schema.Block{
+ "rule": schema.ListNestedBlock{
+ NestedObject: schema.NestedBlockObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "hostname": schema.StringAttribute{
+ Description: "Hosts to apply these rules to",
+ Required: true,
+ },
+ "notes": schema.StringAttribute{
+ Optional: true,
+ },
+ "ip": schema.StringAttribute{
+ Description: "IP or CIDR to block",
+ Required: true,
+ },
+ "action": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("bypass", "log", "challenge", "deny"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ Attributes: map[string]schema.Attribute{
+ "project_id": schema.StringAttribute{
+ Description: "The ID of the project this configuration belongs to.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+ },
+ "team_id": schema.StringAttribute{
+ Description: "The ID of the team this project belongs to.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()},
+ },
+ "enabled": schema.BoolAttribute{
+ Description: "Whether firewall is enabled or not.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(true),
+ },
+ },
+ }
+}
+
+func (r *firewallConfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ "Expected *client.Client, got: %T. Please report this issue to the provider developers.",
+ )
+ return
+ }
+
+ r.client = client
+}
+
+type FirewallConfig struct {
+ ProjectID types.String `tfsdk:"project_id"`
+ TeamID types.String `tfsdk:"team_id"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ ManagedRulesets *FirewallManagedRulesets `tfsdk:"managed_rulesets"`
+
+ Rules *FirewallRules `tfsdk:"rules"`
+ IPRules *IPRules `tfsdk:"ip_rules"`
+}
+
+type FirewallManagedRulesets struct {
+ OWASP *CRSRule `tfsdk:"owasp"`
+}
+
+type CRSRule struct {
+ XSS *CRSRuleConfig `tfsdk:"xss"`
+ SQLI *CRSRuleConfig `tfsdk:"sqli"`
+ LFI *CRSRuleConfig `tfsdk:"lfi"`
+ RFI *CRSRuleConfig `tfsdk:"rfi"`
+ RCE *CRSRuleConfig `tfsdk:"rce"`
+ SD *CRSRuleConfig `tfsdk:"sd"`
+ MA *CRSRuleConfig `tfsdk:"ma"`
+ PHP *CRSRuleConfig `tfsdk:"php"`
+ GEN *CRSRuleConfig `tfsdk:"gen"`
+ JAVA *CRSRuleConfig `tfsdk:"java"`
+}
+
+func (r *CRSRule) ToMap() map[string]*CRSRuleConfig {
+ return map[string]*CRSRuleConfig{
+ "xss": r.XSS,
+ "sqli": r.SQLI,
+ "lfi": r.LFI,
+ "rfi": r.RFI,
+ "rce": r.RCE,
+ "sd": r.SD,
+ "ma": r.MA,
+ "php": r.PHP,
+ "gen": r.GEN,
+ "java": r.JAVA,
+ }
+}
+
+type CRSRuleConfig struct {
+ Active types.Bool `tfsdk:"active"`
+ Action types.String `tfsdk:"action"`
+}
+
+type FirewallRules struct {
+ Rules []FirewallRule `tfsdk:"rule"`
+}
+
+type FirewallRule struct {
+ ID types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Active types.Bool `tfsdk:"active"`
+ ConditionGroup []ConditionGroup `tfsdk:"condition_group"`
+ Action Mitigate `tfsdk:"action"`
+}
+
+func (r *FirewallRule) Conditions() []client.ConditionGroup {
+ var groups []client.ConditionGroup
+ for _, group := range r.ConditionGroup {
+ var conditions []client.Condition
+ for _, condition := range group.Conditions {
+ conditions = append(conditions, client.Condition{
+ Type: condition.Type.ValueString(),
+ Op: condition.Op.ValueString(),
+ Neg: condition.Neg.ValueBool(),
+ Key: condition.Key.ValueString(),
+ Value: condition.Value.ValueString(),
+ })
+ }
+ groups = append(groups, client.ConditionGroup{
+ Conditions: conditions,
+ })
+ }
+ return groups
+}
+
+func (r *FirewallRule) Mitigate() (client.Mitigate, error) {
+ mit := client.Mitigate{
+ Action: r.Action.Action.ValueString(),
+ }
+ if !r.Action.RateLimit.IsNull() {
+ rl := &client.RateLimit{}
+ diags := r.Action.RateLimit.As(context.Background(), rl, basetypes.ObjectAsOptions{
+ UnhandledNullAsEmpty: false,
+ UnhandledUnknownAsEmpty: false,
+ })
+ if diags.HasError() {
+ return mit, fmt.Errorf("error converting rate limit: %s - %s", diags[0].Summary(), diags[0].Detail())
+ }
+ mit.RateLimit = rl
+ }
+
+ if !r.Action.Redirect.IsNull() {
+ rd := &client.Redirect{}
+ diags := r.Action.Redirect.As(context.Background(), rd, basetypes.ObjectAsOptions{
+ UnhandledNullAsEmpty: true,
+ UnhandledUnknownAsEmpty: true,
+ })
+ if diags.HasError() {
+ return mit, fmt.Errorf("error converting rate limit: %s - %s", diags[0].Summary(), diags[0].Detail())
+ }
+ mit.Redirect = rd
+ }
+ if !r.Action.ActionDuration.IsNull() {
+ mit.ActionDuration = r.Action.ActionDuration.ValueString()
+ }
+ return mit, nil
+}
+
+func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) (FirewallRule, error) {
+ var err error
+ r := FirewallRule{
+ ID: types.StringValue(rule.ID),
+ Name: types.StringValue(rule.Name),
+ Description: types.StringValue(rule.Description),
+ Active: types.BoolValue(rule.Active),
+ }
+ if rule.Active && ref.Active == types.BoolNull() {
+ r.Active = ref.Active
+ }
+
+ r.Action, err = fromMitigate(rule.Action.Mitigate, ref.Action)
+ if err != nil {
+ return r, err
+ }
+ var conditionGroups = make([]ConditionGroup, len(rule.ConditionGroup))
+ for j, group := range rule.ConditionGroup {
+ var conditions = make([]Condition, len(group.Conditions))
+ for k, condition := range group.Conditions {
+ var cond = Condition{}
+ if len(ref.ConditionGroup) > j && len(ref.ConditionGroup[j].Conditions) > k {
+ cond = ref.ConditionGroup[j].Conditions[k]
+ }
+ conditions[k] = fromCondition(condition, cond)
+ }
+ conditionGroups[j] = ConditionGroup{
+ Conditions: conditions,
+ }
+ }
+ r.ConditionGroup = conditionGroups
+ // Description and active can be optional
+ if rule.Description == "" && ref.Description == types.StringNull() {
+ r.Description = ref.Description
+ }
+ if rule.Active && ref.Active == types.BoolNull() {
+ r.Active = ref.Active
+ }
+
+ return r, nil
+}
+
+var redirectType = types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "location": types.StringType,
+ "permanent": types.BoolType,
+ },
+}
+
+var ratelimitType = types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "algo": types.StringType,
+ "window": types.Int64Type,
+ "limit": types.Int64Type,
+ "keys": types.ListType{
+ ElemType: types.StringType,
+ },
+ "action": types.StringType,
+ },
+}
+
+type Mitigate struct {
+ Action types.String `tfsdk:"action"`
+ RateLimit types.Object `tfsdk:"rate_limit"`
+ Redirect types.Object `tfsdk:"redirect"`
+ ActionDuration types.String `tfsdk:"action_duration"`
+}
+
+func fromMitigate(mitigate client.Mitigate, ref Mitigate) (Mitigate, error) {
+ m := Mitigate{
+ Action: types.StringValue(mitigate.Action),
+ ActionDuration: types.StringValue(mitigate.ActionDuration),
+ Redirect: types.ObjectNull(redirectType.AttrTypes),
+ RateLimit: types.ObjectNull(ratelimitType.AttrTypes),
+ }
+
+ if mitigate.ActionDuration == "" && ref.ActionDuration == types.StringNull() {
+ m.ActionDuration = ref.ActionDuration
+ }
+
+ if mitigate.RateLimit != nil {
+ keys, diags := basetypes.NewListValueFrom(context.Background(), types.StringType, mitigate.RateLimit.Keys)
+ if diags.HasError() {
+ return m, fmt.Errorf("error converting keys: %s - %s", diags[0].Summary(), diags[0].Detail())
+ }
+ m.RateLimit = types.ObjectValueMust(
+ ratelimitType.AttrTypes,
+ map[string]attr.Value{
+ "algo": types.StringValue(mitigate.RateLimit.Algo),
+ "window": types.Int64Value(mitigate.RateLimit.Window),
+ "limit": types.Int64Value(mitigate.RateLimit.Limit),
+ "keys": keys,
+ "action": types.StringValue(mitigate.RateLimit.Action),
+ },
+ )
+ }
+ if mitigate.Redirect != nil {
+ m.Redirect = types.ObjectValueMust(
+ redirectType.AttrTypes,
+ map[string]attr.Value{
+ "location": types.StringValue(mitigate.Redirect.Location),
+ "permanent": types.BoolValue(mitigate.Redirect.Permanent),
+ },
+ )
+ }
+ return m, nil
+}
+
+type Redirect struct {
+ Location types.String `tfsdk:"location"`
+ Permanent types.Bool `tfsdk:"permanent"`
+}
+
+type RateLimit struct {
+ Algo types.String `tfsdk:"algo"`
+ Window types.Int64 `tfsdk:"window"`
+ Limit types.Int64 `tfsdk:"limit"`
+ Keys types.List `tfsdk:"keys"`
+ Action types.String `tfsdk:"action"`
+}
+
+type ConditionGroup struct {
+ Conditions []Condition `tfsdk:"conditions"`
+}
+
+type Condition struct {
+ Type types.String `tfsdk:"type"`
+ Op types.String `tfsdk:"op"`
+ Neg types.Bool `tfsdk:"neg"`
+ Key types.String `tfsdk:"key"`
+ Value types.String `tfsdk:"value"`
+}
+
+func fromCondition(condition client.Condition, ref Condition) Condition {
+ c := Condition{
+ Type: types.StringValue(condition.Type),
+ Op: types.StringValue(condition.Op),
+ Value: types.StringValue(condition.Value),
+ Key: types.StringValue(condition.Key),
+ Neg: types.BoolValue(condition.Neg),
+ }
+ // Neg and Key are optional
+ if ref.Neg == types.BoolNull() {
+ c.Neg = types.BoolNull()
+ }
+ // if key is present it's possible for value to be optional
+ if ref.Key == types.StringNull() {
+ c.Key = types.StringNull()
+ } else {
+ if ref.Value == types.StringNull() {
+ c.Value = types.StringNull()
+ }
+ }
+ return c
+}
+
+type IPRules struct {
+ Rules []IPRule `tfsdk:"rule"`
+}
+
+type IPRule struct {
+ ID types.String `tfsdk:"id"`
+ Hostname types.String `tfsdk:"hostname"`
+ IP types.String `tfsdk:"ip"`
+ Notes types.String `tfsdk:"notes"`
+ Action types.String `tfsdk:"action"`
+}
+
+func fromCRS(conf map[string]client.CoreRuleSet, refMr *FirewallManagedRulesets) *CRSRule {
+ var ref = &CRSRule{}
+ if refMr != nil && refMr.OWASP != nil {
+ ref = refMr.OWASP
+ }
+ if conf == nil || ref == nil {
+ return nil
+ }
+ return &CRSRule{
+ XSS: fromCoreRuleset(conf["xss"], ref.XSS),
+ SQLI: fromCoreRuleset(conf["sqli"], ref.SQLI),
+ LFI: fromCoreRuleset(conf["lfi"], ref.LFI),
+ RFI: fromCoreRuleset(conf["rfi"], ref.RFI),
+ RCE: fromCoreRuleset(conf["rce"], ref.RCE),
+ SD: fromCoreRuleset(conf["sd"], ref.SD),
+ MA: fromCoreRuleset(conf["ma"], ref.MA),
+ PHP: fromCoreRuleset(conf["php"], ref.PHP),
+ GEN: fromCoreRuleset(conf["gen"], ref.GEN),
+ JAVA: fromCoreRuleset(conf["java"], ref.JAVA),
+ }
+}
+
+func fromCoreRuleset(crsRule client.CoreRuleSet, ref *CRSRuleConfig) *CRSRuleConfig {
+ if ref == nil && !crsRule.Active && crsRule.Action == "log" {
+ return nil
+ }
+ c := &CRSRuleConfig{
+ Active: types.BoolValue(crsRule.Active),
+ Action: types.StringValue(crsRule.Action),
+ }
+ if (ref == nil && crsRule.Active) ||
+ ref != nil && ref.Active == types.BoolNull() {
+ c.Active = types.BoolNull()
+ }
+ return c
+}
+
+func fromClient(conf client.FirewallConfig, state FirewallConfig) (FirewallConfig, error) {
+ var err error
+ cfg := FirewallConfig{
+ ProjectID: state.ProjectID,
+ // Take the teamID from the response/provider if it wasn't provided in resource
+ TeamID: types.StringValue(conf.TeamID),
+ Enabled: state.Enabled,
+ }
+ // Enabled can be null
+ if conf.Enabled && state.Enabled.IsNull() {
+ cfg.Enabled = state.Enabled
+ }
+
+ if len(conf.Rules) > 0 {
+ rules := make([]FirewallRule, len(conf.Rules))
+ for i, rule := range conf.Rules {
+ // Set empty optional types
+ var stateRule = FirewallRule{
+ Active: types.BoolNull(),
+ }
+ if state.Rules != nil && len(state.Rules.Rules)-1 > i {
+ stateRule = state.Rules.Rules[i]
+ }
+ rules[i], err = fromFirewallRule(rule, stateRule)
+ if err != nil {
+ return cfg, err
+ }
+ }
+ cfg.Rules = &FirewallRules{Rules: rules}
+ }
+
+ if len(conf.IPRules) > 0 {
+ ipRules := make([]IPRule, len(conf.IPRules))
+ for i, iprule := range conf.IPRules {
+ ipRules[i] = IPRule{
+ ID: types.StringValue(iprule.ID),
+ Hostname: types.StringValue(iprule.Hostname),
+ IP: types.StringValue(iprule.IP),
+ Notes: types.StringValue(iprule.Notes),
+ Action: types.StringValue(iprule.Action),
+ }
+ // notes don't have to be set
+ if iprule.Notes == "" && state.IPRules != nil && len(state.IPRules.Rules) > i && state.IPRules.Rules[i].Notes.IsNull() {
+ ipRules[i].Notes = state.IPRules.Rules[i].Notes
+ }
+ }
+
+ cfg.IPRules = &IPRules{Rules: ipRules}
+ }
+
+ managedRulesets := &FirewallManagedRulesets{}
+ if conf.ManagedRulesets != nil && conf.CRS != nil {
+ cfg.ManagedRulesets = managedRulesets
+ cfg.ManagedRulesets.OWASP = fromCRS(conf.CRS, state.ManagedRulesets)
+ }
+
+ return cfg, nil
+}
+
+func (f *FirewallConfig) toClient() (client.FirewallConfig, error) {
+ conf := client.FirewallConfig{
+ ProjectID: f.ProjectID.ValueString(),
+ TeamID: f.TeamID.ValueString(),
+ Enabled: f.Enabled.IsNull() || f.Enabled.ValueBool(),
+ }
+
+ if f.ManagedRulesets != nil {
+ conf.ManagedRulesets = make(map[string]client.ManagedRule)
+ if f.ManagedRulesets.OWASP != nil {
+ conf.ManagedRulesets["owasp"] = client.ManagedRule{
+ Active: true,
+ }
+ conf.CRS = make(map[string]client.CoreRuleSet)
+ for key, value := range f.ManagedRulesets.OWASP.ToMap() {
+ if value != nil {
+ conf.CRS[key] = client.CoreRuleSet{
+ Action: value.Action.ValueString(),
+ Active: value.Active.IsNull() || value.Active.ValueBool(),
+ }
+ }
+ }
+ }
+ }
+ if f.Rules != nil && len(f.Rules.Rules) > 0 {
+ for _, rule := range f.Rules.Rules {
+ mit, err := rule.Mitigate()
+ if err != nil {
+ return conf, err
+ }
+ conf.Rules = append(conf.Rules, client.FirewallRule{
+ ID: rule.ID.ValueString(),
+ Name: rule.Name.ValueString(),
+ Description: rule.Description.ValueString(),
+ Active: rule.Active.IsNull() || rule.Active.ValueBool(),
+ ConditionGroup: rule.Conditions(),
+ Action: client.Action{
+ Mitigate: mit,
+ },
+ })
+ }
+ }
+
+ if f.IPRules != nil && len(f.IPRules.Rules) > 0 {
+ for _, iprule := range f.IPRules.Rules {
+ conf.IPRules = append(conf.IPRules, client.IPRule{
+ ID: iprule.ID.ValueString(),
+ Hostname: iprule.Hostname.ValueString(),
+ IP: iprule.IP.ValueString(),
+ Notes: iprule.Notes.ValueString(),
+ Action: iprule.Action.ValueString(),
+ })
+ }
+ }
+ return conf, nil
+}
+
+func (r *firewallConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+
+ var plan FirewallConfig
+ diags := req.Plan.Get(ctx, &plan)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ conf, err := plan.toClient()
+ if err != nil {
+ diags.AddError("failed to convert plan to client", err.Error())
+ return
+ }
+
+ out, err := r.client.PutFirewallConfig(ctx, conf)
+ if err != nil {
+ diags.AddError("failed to create firewall config", err.Error())
+ return
+ }
+
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ cfg, err := fromClient(out, plan)
+ if err != nil {
+ diags.AddError("failed to read created firewall config", err.Error())
+ return
+ }
+ diags = resp.State.Set(ctx, cfg)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state FirewallConfig
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ out, err := r.client.GetFirewallConfig(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString())
+ if err != nil {
+ diags.AddError("failed to read firewall config", err.Error())
+ return
+ }
+ if client.NotFound(err) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ cfg, err := fromClient(out, state)
+ if err != nil {
+ diags.AddError("failed to read firewall config", err.Error())
+ return
+ }
+ diags = resp.State.Set(ctx, cfg)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *firewallConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan FirewallConfig
+ diags := req.Plan.Get(ctx, &plan)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ conf, err := plan.toClient()
+ if err != nil {
+ diags.AddError("failed to convert plan to client", err.Error())
+ return
+ }
+
+ out, err := r.client.PutFirewallConfig(ctx, conf)
+ if err != nil {
+ diags.AddError("failed to create firewall config", err.Error())
+ return
+ }
+
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ cfg, err := fromClient(out, plan)
+ if err != nil {
+ diags.AddError("failed to read updated firewall config", err.Error())
+ return
+ }
+ diags = resp.State.Set(ctx, cfg)
+ resp.Diagnostics.Append(diags...)
+}
+func (r *firewallConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state FirewallConfig
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ conf := client.FirewallConfig{
+ Enabled: false,
+ ProjectID: state.ProjectID.ValueString(),
+ TeamID: state.TeamID.ValueString(),
+ }
+
+ _, err := r.client.PutFirewallConfig(ctx, conf)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete firewall config", err.Error())
+ return
+ }
+ tflog.Info(ctx, "deleted firewall config", map[string]interface{}{
+ "project_id": state.ProjectID.ValueString(),
+ "team_id": state.TeamID.ValueString(),
+ })
+}
+
+func (r *firewallConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ teamID, projectID, ok := splitInto1Or2(req.ID)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Error importing Firewall Config",
+ fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID),
+ )
+ }
+ out, err := r.client.GetFirewallConfig(ctx, projectID, teamID)
+ if err != nil {
+ resp.Diagnostics.AddError("Error importing Firewall Config", err.Error())
+ return
+ }
+ conf, err := fromClient(out, FirewallConfig{
+ ProjectID: types.StringValue(projectID),
+ TeamID: types.StringValue(out.TeamID), // use output teamID if not provided on import
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("failed to read firewall config", err.Error())
+ return
+ }
+ tflog.Info(ctx, "imported firewall config", map[string]interface{}{
+ "team_id": conf.TeamID.ValueString(),
+ "project_id": conf.ProjectID.ValueString(),
+ })
+ diags := resp.State.Set(ctx, conf)
+ resp.Diagnostics.Append(diags...)
+}
diff --git a/vercel/resource_firewall_config_test.go b/vercel/resource_firewall_config_test.go
new file mode 100644
index 00000000..81c64c2a
--- /dev/null
+++ b/vercel/resource_firewall_config_test.go
@@ -0,0 +1,543 @@
+package vercel_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func getFirewallImportID(n string) resource.ImportStateIdFunc {
+ return func(s *terraform.State) (string, error) {
+ rs, ok := s.RootModule().Resources[n]
+ if !ok {
+ return "", fmt.Errorf("not found: %s", n)
+ }
+
+ if rs.Primary.ID == "" {
+ return "", fmt.Errorf("no ID is set")
+ }
+
+ if rs.Primary.Attributes["team_id"] == "" {
+ return rs.Primary.ID, nil
+ }
+ return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["project_id"]), nil
+ }
+}
+
+func TestAcc_FirewallConfigResource(t *testing.T) {
+ name := acctest.RandString(16)
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccFirewallConfigResource(name, teamIDConfig()),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.xss.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.sqli.action",
+ "log"),
+ resource.TestCheckNoResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.sqli.active"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.rce.active",
+ "false"),
+ resource.TestCheckNoResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.php"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "enabled",
+ "true"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.name",
+ "test"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.id",
+ "rule_test"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.action.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.condition_group.0.conditions.0.type",
+ "path"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.condition_group.0.conditions.1.value",
+ "POST"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.id",
+ "rule_test2"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.action",
+ "rate_limit"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.limit",
+ "100"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.algo",
+ "fixed_window"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.action",
+ "challenge"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.window",
+ "60"),
+ resource.TestCheckTypeSetElemAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.keys.*",
+ "ip"),
+ resource.TestCheckTypeSetElemAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.keys.*",
+ "ja4"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.id",
+ "rule_test3"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.action.redirect.location",
+ "/bye"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.action.redirect.permanent",
+ "false"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.0.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.0.hostname",
+ "test.com"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.1.ip",
+ "1.2.3.4/32"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.1.hostname",
+ "*.test.com"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.2.ip",
+ "2.4.6.8"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.2.hostname",
+ "*"),
+ ),
+ },
+ {
+ ImportState: true,
+ ResourceName: "vercel_firewall_config.managed",
+ ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.managed"),
+ },
+ {
+ ImportState: true,
+ ResourceName: "vercel_firewall_config.custom",
+ ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.custom"),
+ },
+ {
+ ImportState: true,
+ ResourceName: "vercel_firewall_config.ips",
+ ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.ips"),
+ },
+ {
+ Config: testAccFirewallConfigResourceUpdated(name, teamIDConfig()),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.xss.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.sqli.action",
+ "deny"),
+ resource.TestCheckNoResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.sqli.active"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.rce.active",
+ "false"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.php.action",
+ "log"),
+ resource.TestCheckNoResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.php.active"),
+ resource.TestCheckNoResourceAttr(
+ "vercel_firewall_config.managed",
+ "managed_rulesets.owasp.java"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "enabled",
+ "true"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.name",
+ "test1"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.id",
+ "rule_test1"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.action.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.condition_group.0.conditions.0.type",
+ "path"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.0.condition_group.0.conditions.1.value",
+ "POST"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.id",
+ "rule_test2"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.action",
+ "rate_limit"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.limit",
+ "150"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.algo",
+ "fixed_window"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.action",
+ "challenge"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.window",
+ "60"),
+ resource.TestCheckTypeSetElemAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.1.action.rate_limit.keys.*",
+ "ip"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.id",
+ "rule_test3"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.action.redirect.location",
+ "/bye"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.custom",
+ "rules.rule.2.action.redirect.permanent",
+ "false"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.0.action",
+ "deny"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.0.hostname",
+ "test.com"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.1.ip",
+ "1.2.3.4/32"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.1.hostname",
+ "*.test.com"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.2.ip",
+ "2.4.6.8/32"),
+ resource.TestCheckResourceAttr(
+ "vercel_firewall_config.ips",
+ "ip_rules.rule.2.hostname",
+ "*"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccFirewallConfigResource(name, teamID string) string {
+ return fmt.Sprintf(`
+resource "vercel_project" "managed" {
+ name = "test-acc-%[1]s-mrs"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "managed" {
+ project_id = vercel_project.managed.id
+ %[2]s
+
+ managed_rulesets {
+ owasp {
+ xss = { action = "deny" }
+ sqli = { action = "log" }
+
+ rce = { action = "deny", active = false }
+ }
+ }
+}
+
+resource "vercel_project" "custom" {
+ name = "test-acc-%[1]s-custom"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "custom" {
+ project_id = vercel_project.custom.id
+ %[2]s
+
+ rules {
+ rule {
+ name = "test"
+ action = {
+ action = "deny"
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/test"
+ },
+ {
+ type = "method"
+ op = "eq"
+ value = "POST"
+ }]
+ }]
+ }
+ rule {
+ name = "test2"
+ action = {
+ action = "rate_limit"
+ rate_limit = {
+ limit = 100
+ window = 60
+ algo = "fixed_window"
+ keys = ["ip", "ja4"]
+ action = "challenge"
+
+ }
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ }]
+ }]
+ }
+ rule {
+ name = "test3"
+ action = {
+ action = "redirect"
+ redirect = {
+ location = "/bye"
+ permanent = false
+
+ }
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ }]
+ }]
+ }
+
+ rule {
+ name = "test4"
+ action = {
+ action = "log"
+ }
+ condition_group = [{
+ conditions = [{
+ type = "ja4_digest"
+ op = "eq"
+ value = "fakeja4"
+ }]
+ }]
+ }
+ }
+}
+
+resource "vercel_project" "ips" {
+ name = "test-acc-%[1]s-ips"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "ips" {
+ project_id = vercel_project.ips.id
+ %[2]s
+
+ ip_rules {
+ rule {
+ action = "deny"
+ ip = "5.5.0.0/16"
+ hostname = "test.com"
+ }
+ rule {
+ action = "deny"
+ ip = "1.2.3.4/32"
+ hostname = "*.test.com"
+ }
+ rule {
+ action = "deny"
+ ip = "2.4.6.8"
+ hostname = "*"
+ }
+ }
+}
+
+`, name, teamID)
+}
+
+func testAccFirewallConfigResourceUpdated(name, teamID string) string {
+ return fmt.Sprintf(`
+resource "vercel_project" "managed" {
+ name = "test-acc-%[1]s-mrs"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "managed" {
+ project_id = vercel_project.managed.id
+ %[2]s
+
+ managed_rulesets {
+ owasp {
+ xss = { action = "deny", active = false }
+ sqli = { action = "deny" }
+
+ rce = { action = "deny", active = false }
+ php = { action = "log" }
+ }
+ }
+}
+
+resource "vercel_project" "custom" {
+ name = "test-acc-%[1]s-custom"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "custom" {
+ project_id = vercel_project.custom.id
+ %[2]s
+
+ rules {
+ rule {
+ name = "test1"
+ action = {
+ action = "deny"
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/test"
+ },
+ {
+ type = "method"
+ op = "eq"
+ value = "POST"
+ }]
+ }]
+ }
+ rule {
+ name = "test2"
+ action = {
+ action = "rate_limit"
+ rate_limit = {
+ limit = 150
+ window = 60
+ algo = "fixed_window"
+ keys = ["ip"]
+ action = "challenge"
+
+ }
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ }]
+ }]
+ }
+ rule {
+ name = "test3"
+ action = {
+ action = "redirect"
+ redirect = {
+ location = "/bye"
+ permanent = false
+
+ }
+ }
+ condition_group = [{
+ conditions = [{
+ type = "path"
+ op = "eq"
+ value = "/api"
+ }]
+ }]
+ }
+ }
+}
+
+resource "vercel_project" "ips" {
+ name = "test-acc-%[1]s-ips"
+ %[2]s
+}
+
+resource "vercel_firewall_config" "ips" {
+ project_id = vercel_project.ips.id
+ %[2]s
+
+ ip_rules {
+ rule {
+ action = "deny"
+ ip = "5.6.0.0/16"
+ hostname = "test.com"
+ }
+ rule {
+ action = "challenge"
+ ip = "1.2.3.4/32"
+ hostname = "*.test.com"
+ }
+ rule {
+ action = "deny"
+ ip = "2.4.6.8/32"
+ hostname = "*"
+ }
+ }
+}`, name, teamID)
+}