From 01214aaed4c3cc43e0730c73e7667a3c70696786 Mon Sep 17 00:00:00 2001 From: sueplex Date: Thu, 25 Jul 2024 17:12:52 -0400 Subject: [PATCH 01/17] firewall wip --- go.mod | 7 +- go.sum | 14 +- vercel/provider.go | 1 + vercel/resource_attack_challenge_mode.go | 2 +- vercel/resource_firewall_config.go | 878 +++++++++++++++++++++++ 5 files changed, 892 insertions(+), 10 deletions(-) create mode 100644 vercel/resource_firewall_config.go 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 4d024486..08b188a6 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, newProjectDomainResource, newProjectEnvironmentVariableResource, 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..65ffd6e3 --- /dev/null +++ b/vercel/resource_firewall_config.go @@ -0,0 +1,878 @@ +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/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-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{ + Attributes: map[string]schema.Attribute{ + "xss": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "sqli": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "lfi": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "rfi": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "rce": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "sd": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "ma": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "php": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "gen": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + "java": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + "rules": schema.SingleNestedBlock{ + 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{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(4, 160), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(260), + }, + }, + "active": schema.BoolAttribute{ + Optional: true, + }, + "action": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit", "redirect"), + }, + }, + "rate_limit": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "algo": schema.StringAttribute{ + Required: true, + }, + "window": schema.Int64Attribute{ + Required: true, + }, + "limit": schema.Int64Attribute{ + Required: true, + }, + "keys": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + "action": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit"), + }, + }, + }, + }, + "redirect": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "location": schema.StringAttribute{ + Required: true, + }, + "permanent": schema.BoolAttribute{ + Required: true, + }, + }, + }, + "action_duration": schema.StringAttribute{ + Optional: true, + }, + }, + }, + "condition_group": schema.ListNestedAttribute{ + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "conditions": schema.ListNestedAttribute{ + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + 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{ + 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{ + Optional: true, + }, + "value": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "ip_rules": schema.SingleNestedBlock{ + Description: "IP rules to apply to the project.", + Blocks: map[string]schema.Block{ + "rule": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "hostname": schema.StringAttribute{ + Required: true, + }, + "notes": schema.StringAttribute{ + Optional: true, + }, + "ip": schema.StringAttribute{ + 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 { + mit := client.Mitigate{ + Action: r.Action.Action.ValueString(), + } + if r.Action.RateLimit != nil { + keys := make([]string, len(r.Action.RateLimit.Keys)) + for i, k := range r.Action.RateLimit.Keys { + keys[i] = k.ValueString() + } + mit.RateLimit = &client.RateLimit{ + Algo: r.Action.RateLimit.Algo.ValueString(), + Window: r.Action.RateLimit.Window.ValueInt64(), + Limit: r.Action.RateLimit.Limit.ValueInt64(), + Keys: keys, + Action: r.Action.RateLimit.Action.ValueString(), + } + } + if r.Action.Redirect != nil { + mit.Redirect = &client.Redirect{ + Location: r.Action.Redirect.Location.ValueString(), + Permanent: r.Action.Redirect.Permanent.ValueBool(), + } + } + if !r.Action.ActionDuration.IsNull() { + mit.ActionDuration = r.Action.ActionDuration.ValueString() + } + return mit +} + +func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { + 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 == true && ref.Active == types.BoolNull() { + r.Active = ref.Active + } + + r.Action = fromMitigate(rule.Action.Mitigate, ref.Action) + 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 == true && ref.Active == types.BoolNull() { + r.Active = ref.Active + } + + return r +} + +type Mitigate struct { + Action types.String `tfsdk:"action"` + RateLimit *RateLimit `tfsdk:"rate_limit"` + Redirect *Redirect `tfsdk:"redirect"` + ActionDuration types.String `tfsdk:"action_duration"` +} + +func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { + m := Mitigate{ + Action: types.StringValue(mitigate.Action), + ActionDuration: types.StringValue(mitigate.ActionDuration), + } + + if mitigate.ActionDuration == "" && ref.ActionDuration == types.StringNull() { + m.ActionDuration = ref.ActionDuration + } + + if mitigate.RateLimit != nil { + keys := make([]types.String, len(mitigate.RateLimit.Keys)) + for i, k := range mitigate.RateLimit.Keys { + keys[i] = types.StringValue(k) + } + m.RateLimit = &RateLimit{ + 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 = &Redirect{ + Location: types.StringValue(mitigate.Redirect.Location), + Permanent: types.BoolValue(mitigate.Redirect.Permanent), + } + } + return m +} + +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.String `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 ref.Key == types.StringNull() { + c.Key = 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 == false && crsRule.Action == "log" { + return nil + } + c := &CRSRuleConfig{ + Active: types.BoolValue(crsRule.Active), + Action: types.StringValue(crsRule.Action), + } + if (ref == nil && crsRule.Active == true) || + ref != nil && ref.Active == types.BoolNull() { + c.Active = types.BoolNull() + } + return c +} + +func fromClient(conf client.FirewallConfig, state FirewallConfig) FirewallConfig { + 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 + } + + 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] = fromFirewallRule(rule, stateRule) + + } + cfg.Rules = &FirewallRules{Rules: rules} + + 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 +} + +func (f *FirewallConfig) toClient() client.FirewallConfig { + 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 { + 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: rule.Mitigate(), + }, + }) + } + } + + 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 +} + +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 := plan.toClient() + + out, err := r.client.PutFirewallConfig(ctx, conf) + if err != nil { + diags.AddError("failed to create firewall config", err.Error()) + } + + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + cfg := fromClient(out, plan) + diags = resp.State.Set(ctx, cfg) + resp.Diagnostics.Append(diags...) + return +} + +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()) + } + cfg := fromClient(out, state) + diags = resp.State.Set(ctx, cfg) + resp.Diagnostics.Append(diags...) + return +} + +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 := plan.toClient() + + out, err := r.client.PutFirewallConfig(ctx, conf) + if err != nil { + diags.AddError("failed to create firewall config", err.Error()) + } + + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + cfg := fromClient(out, plan) + diags = resp.State.Set(ctx, cfg) + resp.Diagnostics.Append(diags...) + return +} +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()) + } + tflog.Info(ctx, "deleted firewall config", map[string]interface{}{ + "project_id": state.ProjectID.ValueString(), + "team_id": state.TeamID.ValueString(), + }) + return +} +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 := fromClient(out, FirewallConfig{ + ProjectID: types.StringValue(projectID), + TeamID: types.StringValue(out.TeamID), // use output teamID if not provided on import + }) + 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...) +} From 4ef25021b4cd7ce1ab1c4367822512f4396d2c59 Mon Sep 17 00:00:00 2001 From: sueplex Date: Thu, 25 Jul 2024 20:16:37 -0400 Subject: [PATCH 02/17] client and tests --- client/firewall_config.go | 123 ++++++ vercel/resource_firewall_config.go | 58 +-- vercel/resource_firewall_config_test.go | 543 ++++++++++++++++++++++++ 3 files changed, 697 insertions(+), 27 deletions(-) create mode 100644 client/firewall_config.go create mode 100644 vercel/resource_firewall_config_test.go diff --git a/client/firewall_config.go b/client/firewall_config.go new file mode 100644 index 00000000..d39bb169 --- /dev/null +++ b/client/firewall_config.go @@ -0,0 +1,123 @@ +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"` + Window int64 `json:"window"` + Limit int64 `json:"limit"` + Keys []string `json:"keys"` + Action string `json:"action"` +} + +type Redirect struct { + Location string `json:"location"` + Permanent bool `json:"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/security/firewall/config?projectId=%s&teamId=%s", + c.baseURL, + projectId, + teamId, + ) + var res struct { + Active FirewallConfig `json:"active"` + } + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &res) + res.Active.TeamID = teamId + + return res.Active, err +} + +func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (FirewallConfig, error) { + teamId := c.teamID(cfg.TeamID) + url := fmt.Sprintf( + "%s/security/firewall/%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/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 65ffd6e3..2f0d4bb7 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -312,7 +312,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge "ip_rules": schema.SingleNestedBlock{ Description: "IP rules to apply to the project.", Blocks: map[string]schema.Block{ - "rule": schema.SetNestedBlock{ + "rule": schema.ListNestedBlock{ NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -669,36 +669,40 @@ func fromClient(conf client.FirewallConfig, state FirewallConfig) FirewallConfig cfg.Enabled = state.Enabled } - 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] = fromFirewallRule(rule, stateRule) - - } - cfg.Rules = &FirewallRules{Rules: rules} + 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] = fromFirewallRule(rule, stateRule) - 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.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} + cfg.IPRules = &IPRules{Rules: ipRules} + } managedRulesets := &FirewallManagedRulesets{} if conf.ManagedRulesets != nil && conf.CRS != nil { diff --git a/vercel/resource_firewall_config_test.go b/vercel/resource_firewall_config_test.go new file mode 100644 index 00000000..bce41dd2 --- /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 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: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_firewall_config.managed"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + ImportState: true, + ResourceName: "vercel_firewall_config.custom", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_firewall_config.custom"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + ImportState: true, + ResourceName: "vercel_firewall_config.ips", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_firewall_config.ips"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + 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) +} From 6620d6aef644adef24ef3d583999c7396e52d7d0 Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 26 Jul 2024 11:34:50 -0400 Subject: [PATCH 03/17] ineffassigns --- vercel/resource_firewall_config.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 2f0d4bb7..a71f39ba 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -495,7 +495,7 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { Description: types.StringValue(rule.Description), Active: types.BoolValue(rule.Active), } - if rule.Active == true && ref.Active == types.BoolNull() { + if rule.Active && ref.Active == types.BoolNull() { r.Active = ref.Active } @@ -519,7 +519,7 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { if rule.Description == "" && ref.Description == types.StringNull() { r.Description = ref.Description } - if rule.Active == true && ref.Active == types.BoolNull() { + if rule.Active && ref.Active == types.BoolNull() { r.Active = ref.Active } @@ -643,14 +643,14 @@ func fromCRS(conf map[string]client.CoreRuleSet, refMr *FirewallManagedRulesets) } func fromCoreRuleset(crsRule client.CoreRuleSet, ref *CRSRuleConfig) *CRSRuleConfig { - if ref == nil && crsRule.Active == false && crsRule.Action == "log" { + 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 == true) || + if (ref == nil && crsRule.Active) || ref != nil && ref.Active == types.BoolNull() { c.Active = types.BoolNull() } @@ -788,7 +788,6 @@ func (r *firewallConfigResource) Create(ctx context.Context, req resource.Create cfg := fromClient(out, plan) diags = resp.State.Set(ctx, cfg) resp.Diagnostics.Append(diags...) - return } func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -806,7 +805,6 @@ func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequ cfg := fromClient(out, state) diags = resp.State.Set(ctx, cfg) resp.Diagnostics.Append(diags...) - return } func (r *firewallConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -830,7 +828,6 @@ func (r *firewallConfigResource) Update(ctx context.Context, req resource.Update cfg := fromClient(out, plan) diags = resp.State.Set(ctx, cfg) resp.Diagnostics.Append(diags...) - return } func (r *firewallConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state FirewallConfig @@ -854,7 +851,6 @@ func (r *firewallConfigResource) Delete(ctx context.Context, req resource.Delete "project_id": state.ProjectID.ValueString(), "team_id": state.TeamID.ValueString(), }) - return } func (r *firewallConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { teamID, projectID, ok := splitInto1Or2(req.ID) From a67d824e742afb094e759664efa3f6f3d211e013 Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 26 Jul 2024 11:41:07 -0400 Subject: [PATCH 04/17] wip gen docs --- docs/resources/attack_challenge_mode.md | 2 +- docs/resources/firewall_config.md | 280 ++++++++++++++++++++++++ 2 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 docs/resources/firewall_config.md 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..c7fa6bbc --- /dev/null +++ b/docs/resources/firewall_config.md @@ -0,0 +1,280 @@ +--- +# 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. + + + + +## 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) (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) +- `ip` (String) + +Optional: + +- `notes` (String) + +Read-Only: + +- `id` (String) The ID of this resource. + + + + +### Nested Schema for `managed_rulesets` + +Optional: + +- `owasp` (Block, Optional) (see [below for nested schema](#nestedblock--managed_rulesets--owasp)) + + +### Nested Schema for `managed_rulesets.owasp` + +Optional: + +- `gen` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--gen)) +- `java` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--java)) +- `lfi` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--lfi)) +- `ma` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--ma)) +- `php` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--php)) +- `rce` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rce)) +- `rfi` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rfi)) +- `sd` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sd)) +- `sqli` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sqli)) +- `xss` (Attributes) (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) (see [below for nested schema](#nestedatt--rules--rule--action)) +- `condition_group` (Attributes List) (see [below for nested schema](#nestedatt--rules--rule--condition_group)) +- `name` (String) + +Optional: + +- `active` (Boolean) +- `description` (String) + +Read-Only: + +- `id` (String) The ID of this resource. + + +### Nested Schema for `rules.rule.action` + +Required: + +- `action` (String) + +Optional: + +- `action_duration` (String) +- `rate_limit` (Attributes) (see [below for nested schema](#nestedatt--rules--rule--action--rate_limit)) +- `redirect` (Attributes) (see [below for nested schema](#nestedatt--rules--rule--action--redirect)) + + +### Nested Schema for `rules.rule.action.rate_limit` + +Required: + +- `action` (String) +- `algo` (String) +- `keys` (List of String) +- `limit` (Number) +- `window` (Number) + + + +### Nested Schema for `rules.rule.action.redirect` + +Required: + +- `location` (String) +- `permanent` (Boolean) + + + + +### Nested Schema for `rules.rule.condition_group` + +Required: + +- `conditions` (Attributes List) (see [below for nested schema](#nestedatt--rules--rule--condition_group--conditions)) + + +### Nested Schema for `rules.rule.condition_group.conditions` + +Required: + +- `op` (String) +- `type` (String) +- `value` (String) + +Optional: + +- `key` (String) +- `neg` (Boolean) From 0083527145bbeed7335e942223b6bd915c97e2d6 Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 26 Jul 2024 12:06:38 -0400 Subject: [PATCH 05/17] 404 and more descriptions --- vercel/resource_firewall_config.go | 60 +++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index a71f39ba..f6bdb656 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -43,6 +43,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge 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, @@ -159,6 +160,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "rules": schema.SingleNestedBlock{ + Description: "Custom rules to apply to the project", Blocks: map[string]schema.Block{ "rule": schema.ListNestedBlock{ Validators: []validator.List{ @@ -170,7 +172,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Computed: true, }, "name": schema.StringAttribute{ - Required: true, + Description: "Name to identify the rule", + Required: true, Validators: []validator.String{ stringvalidator.LengthBetween(4, 160), }, @@ -182,35 +185,44 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "active": schema.BoolAttribute{ - Optional: true, + Description: "Whether the rule is active or not", + Optional: true, }, "action": schema.SingleNestedAttribute{ - Required: true, + Description: "Actions to take when the condition groups match a request", + Required: true, Attributes: map[string]schema.Attribute{ "action": schema.StringAttribute{ - Required: true, + Description: "Base action", + Required: true, Validators: []validator.String{ stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit", "redirect"), }, }, "rate_limit": schema.SingleNestedAttribute{ - Optional: true, + Description: "Behavior or a rate limiting action. Required if action is rate_limit", + Optional: true, Attributes: map[string]schema.Attribute{ "algo": schema.StringAttribute{ - Required: true, + Description: "Rate limiting algorithm", + Required: true, }, "window": schema.Int64Attribute{ - Required: true, + Description: "Time window in seconds", + Required: true, }, "limit": schema.Int64Attribute{ - Required: true, + 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{ - Required: true, + Description: "Action taken when rate limit is exceeded", + Required: true, Validators: []validator.String{ stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit"), }, @@ -218,7 +230,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "redirect": schema.SingleNestedAttribute{ - Optional: true, + Description: "How to redirect a request. Required if action is redirect", + Optional: true, Attributes: map[string]schema.Attribute{ "location": schema.StringAttribute{ Required: true, @@ -229,20 +242,24 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "action_duration": schema.StringAttribute{ - Optional: true, + Description: "Forward persistence of a rule aciton", + Optional: true, }, }, }, "condition_group": schema.ListNestedAttribute{ - Required: true, + Description: "Sets of conditions that may match a request", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "conditions": schema.ListNestedAttribute{ - Required: true, + Description: "Conditions that must all match within a group", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ - Required: true, + Description: "Request key type to match against", + Required: true, Validators: []validator.String{ stringvalidator.OneOf( "host", @@ -269,7 +286,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "op": schema.StringAttribute{ - Required: true, + Description: "How to comparse type to value", + Required: true, Validators: []validator.String{ stringvalidator.OneOf( "re", @@ -293,7 +311,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Optional: true, }, "key": schema.StringAttribute{ - Optional: true, + Description: "Key within type to match against", + Optional: true, }, "value": schema.StringAttribute{ Required: true, @@ -319,13 +338,15 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Computed: true, }, "hostname": schema.StringAttribute{ - Required: true, + Description: "Hosts to apply these rules to", + Required: true, }, "notes": schema.StringAttribute{ Optional: true, }, "ip": schema.StringAttribute{ - Required: true, + Description: "IP or CIDR to block", + Required: true, }, "action": schema.StringAttribute{ Required: true, @@ -802,6 +823,9 @@ func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequ if err != nil { diags.AddError("failed to read firewall config", err.Error()) } + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + } cfg := fromClient(out, state) diags = resp.State.Set(ctx, cfg) resp.Diagnostics.Append(diags...) From 6afe5f5e4f805c9ab9fe5f518222bcb98f7abb8f Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 26 Jul 2024 12:08:29 -0400 Subject: [PATCH 06/17] docgen --- docs/resources/firewall_config.md | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/resources/firewall_config.md b/docs/resources/firewall_config.md index c7fa6bbc..341975f3 100644 --- a/docs/resources/firewall_config.md +++ b/docs/resources/firewall_config.md @@ -24,7 +24,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge - `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) (see [below for nested schema](#nestedblock--rules)) +- `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. @@ -40,8 +40,8 @@ Optional: Required: - `action` (String) -- `hostname` (String) -- `ip` (String) +- `hostname` (String) Hosts to apply these rules to +- `ip` (String) IP or CIDR to block Optional: @@ -58,7 +58,7 @@ Read-Only: Optional: -- `owasp` (Block, Optional) (see [below for nested schema](#nestedblock--managed_rulesets--owasp)) +- `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` @@ -210,13 +210,13 @@ Optional: Required: -- `action` (Attributes) (see [below for nested schema](#nestedatt--rules--rule--action)) -- `condition_group` (Attributes List) (see [below for nested schema](#nestedatt--rules--rule--condition_group)) -- `name` (String) +- `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) +- `active` (Boolean) Whether the rule is active or not - `description` (String) Read-Only: @@ -228,24 +228,24 @@ Read-Only: Required: -- `action` (String) +- `action` (String) Base action Optional: -- `action_duration` (String) -- `rate_limit` (Attributes) (see [below for nested schema](#nestedatt--rules--rule--action--rate_limit)) -- `redirect` (Attributes) (see [below for nested schema](#nestedatt--rules--rule--action--redirect)) +- `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) -- `algo` (String) -- `keys` (List of String) -- `limit` (Number) -- `window` (Number) +- `action` (String) Action taken 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 @@ -263,18 +263,18 @@ Required: Required: -- `conditions` (Attributes List) (see [below for nested schema](#nestedatt--rules--rule--condition_group--conditions)) +- `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) -- `type` (String) +- `op` (String) How to comparse type to value +- `type` (String) Request key type to match against - `value` (String) Optional: -- `key` (String) +- `key` (String) Key within type to match against - `neg` (Boolean) From 55173ca424235fe6bd22f90babf206a9013b535c Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 26 Jul 2024 12:15:26 -0400 Subject: [PATCH 07/17] missign return --- vercel/resource_firewall_config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index f6bdb656..6129712a 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -825,6 +825,7 @@ func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequ } if client.NotFound(err) { resp.State.RemoveResource(ctx) + return } cfg := fromClient(out, state) diags = resp.State.Set(ctx, cfg) From 76803528696ed12a505df1786cd5e816aaba9156 Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 9 Aug 2024 15:59:19 -0400 Subject: [PATCH 08/17] add version --- client/firewall_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index d39bb169..76c67afb 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -79,7 +79,7 @@ type CoreRuleSet struct { func (c *Client) GetFirewallConfig(ctx context.Context, projectId string, teamId string) (FirewallConfig, error) { teamId = c.teamID(teamId) url := fmt.Sprintf( - "%s/security/firewall/config?projectId=%s&teamId=%s", + "%s/v1/security/firewall/config?projectId=%s&teamId=%s", c.baseURL, projectId, teamId, From cabba3c3806d576d54c2b2be0a78f9e0d97c9408 Mon Sep 17 00:00:00 2001 From: sueplex Date: Tue, 13 Aug 2024 01:03:41 -0400 Subject: [PATCH 09/17] use correct API routes and refernce proper proejctID on import --- client/firewall_config.go | 3 +- vercel/resource_firewall_config.go | 1 + vercel/resource_firewall_config_test.go | 54 ++++++++++++------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index 76c67afb..a6a02941 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -93,14 +93,13 @@ func (c *Client) GetFirewallConfig(ctx context.Context, projectId string, teamId url: url, }, &res) res.Active.TeamID = teamId - return res.Active, err } func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (FirewallConfig, error) { teamId := c.teamID(cfg.TeamID) url := fmt.Sprintf( - "%s/security/firewall/%s?teamId=%s", + "%s/v1/security/firewall/%s?teamId=%s", c.baseURL, cfg.ProjectID, teamId, diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 6129712a..d25bf461 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -877,6 +877,7 @@ func (r *firewallConfigResource) Delete(ctx context.Context, req resource.Delete "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 { diff --git a/vercel/resource_firewall_config_test.go b/vercel/resource_firewall_config_test.go index bce41dd2..81c64c2a 100644 --- a/vercel/resource_firewall_config_test.go +++ b/vercel/resource_firewall_config_test.go @@ -9,6 +9,24 @@ import ( "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{ @@ -131,37 +149,19 @@ func TestAcc_FirewallConfigResource(t *testing.T) { ), }, { - ImportState: true, - ResourceName: "vercel_firewall_config.managed", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources["vercel_firewall_config.managed"] - if !ok { - return "", fmt.Errorf("resource not found") - } - return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil - }, + ImportState: true, + ResourceName: "vercel_firewall_config.managed", + ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.managed"), }, { - ImportState: true, - ResourceName: "vercel_firewall_config.custom", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources["vercel_firewall_config.custom"] - if !ok { - return "", fmt.Errorf("resource not found") - } - return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil - }, + ImportState: true, + ResourceName: "vercel_firewall_config.custom", + ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.custom"), }, { - ImportState: true, - ResourceName: "vercel_firewall_config.ips", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources["vercel_firewall_config.ips"] - if !ok { - return "", fmt.Errorf("resource not found") - } - return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil - }, + ImportState: true, + ResourceName: "vercel_firewall_config.ips", + ImportStateIdFunc: getFirewallImportID("vercel_firewall_config.ips"), }, { Config: testAccFirewallConfigResourceUpdated(name, teamIDConfig()), From d53c5f7ac2c59308d63b954d858ef6435fa61503 Mon Sep 17 00:00:00 2001 From: sueplex Date: Tue, 13 Aug 2024 01:15:13 -0400 Subject: [PATCH 10/17] use versioned endpoint for get --- client/firewall_config.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index a6a02941..414b6ae2 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -79,21 +79,19 @@ type CoreRuleSet struct { 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?projectId=%s&teamId=%s", + "%s/v1/security/firewall/config/active?projectId=%s&teamId=%s", c.baseURL, projectId, teamId, ) - var res struct { - Active FirewallConfig `json:"active"` - } + var res = FirewallConfig{} err := c.doRequest(clientRequest{ ctx: ctx, method: "GET", url: url, }, &res) - res.Active.TeamID = teamId - return res.Active, err + res.TeamID = teamId + return res, err } func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (FirewallConfig, error) { From d8d84013160e8de39616e2f6685e2aa83d7fd738 Mon Sep 17 00:00:00 2001 From: sueplex Date: Tue, 13 Aug 2024 17:34:12 -0400 Subject: [PATCH 11/17] one more api endpoint udpate --- client/firewall_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index 414b6ae2..2944d4e6 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -97,7 +97,7 @@ func (c *Client) GetFirewallConfig(ctx context.Context, projectId string, teamId func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (FirewallConfig, error) { teamId := c.teamID(cfg.TeamID) url := fmt.Sprintf( - "%s/v1/security/firewall/%s?teamId=%s", + "%s/v1/security/firewall/config?projectId=%s&teamId=%s", c.baseURL, cfg.ProjectID, teamId, From 51d7b018c031d9813af2be4758ace22396452bf5 Mon Sep 17 00:00:00 2001 From: sueplex Date: Tue, 3 Sep 2024 19:17:52 -0400 Subject: [PATCH 12/17] working --- client/firewall_config.go | 16 ++-- vercel/provider_test.go | 2 +- vercel/resource_firewall_config.go | 117 ++++++++++++++++++++--------- 3 files changed, 90 insertions(+), 45 deletions(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index 2944d4e6..4ebfbc89 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -51,16 +51,16 @@ type Mitigate struct { } type RateLimit struct { - Algo string `json:"algo"` - Window int64 `json:"window"` - Limit int64 `json:"limit"` - Keys []string `json:"keys"` - Action string `json:"action"` + 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"` - Permanent bool `json:"permanent"` + Location string `json:"location" tfsdk:"location"` + Permanent bool `json:"permanent" tfsdk:"permanent"` } type IPRule struct { @@ -109,6 +109,8 @@ func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (Fir } payload := mustMarshal(cfg) + //fmt.Println("Payload: ", string(payload)) + err := c.doRequest(clientRequest{ ctx: ctx, method: "PUT", diff --git a/vercel/provider_test.go b/vercel/provider_test.go index c5c98b00..d1ec38c0 100644 --- a/vercel/provider_test.go +++ b/vercel/provider_test.go @@ -17,7 +17,7 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe func mustHaveEnv(t *testing.T, name string) { if os.Getenv(name) == "" { - t.Fatalf("%s environment variable must be set for acceptance tests", name) + //t.Fatalf("%s environment variable must be set for acceptance tests", name) } } diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index d25bf461..b78f334f 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -6,6 +6,7 @@ import ( "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" @@ -13,6 +14,7 @@ import ( "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" ) @@ -484,24 +486,28 @@ func (r *FirewallRule) Mitigate() client.Mitigate { mit := client.Mitigate{ Action: r.Action.Action.ValueString(), } - if r.Action.RateLimit != nil { - keys := make([]string, len(r.Action.RateLimit.Keys)) - for i, k := range r.Action.RateLimit.Keys { - keys[i] = k.ValueString() - } - mit.RateLimit = &client.RateLimit{ - Algo: r.Action.RateLimit.Algo.ValueString(), - Window: r.Action.RateLimit.Window.ValueInt64(), - Limit: r.Action.RateLimit.Limit.ValueInt64(), - Keys: keys, - Action: r.Action.RateLimit.Action.ValueString(), + fmt.Println(r) + if !r.Action.RateLimit.IsNull() { + fmt.Println("RateLimit is not null") + rl := &client.RateLimit{} + diags := r.Action.RateLimit.As(context.Background(), rl, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + if diags.HasError() { + fmt.Println("Has error") + fmt.Println(diags) } + mit.RateLimit = rl } - if r.Action.Redirect != nil { - mit.Redirect = &client.Redirect{ - Location: r.Action.Redirect.Location.ValueString(), - Permanent: r.Action.Redirect.Permanent.ValueBool(), - } + + if !r.Action.Redirect.IsNull() { + rd := &client.Redirect{} + r.Action.Redirect.As(context.Background(), rd, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + mit.Redirect = rd } if !r.Action.ActionDuration.IsNull() { mit.ActionDuration = r.Action.ActionDuration.ValueString() @@ -547,10 +553,37 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { return r } +/* + type Mitigate struct { + Action types.String `tfsdk:"action"` + RateLimit *RateLimit `tfsdk:"rate_limit"` + Redirect *Redirect `tfsdk:"redirect"` + ActionDuration types.String `tfsdk:"action_duration"` + } +*/ +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 *RateLimit `tfsdk:"rate_limit"` - Redirect *Redirect `tfsdk:"redirect"` + RateLimit types.Object `tfsdk:"rate_limit"` + Redirect types.Object `tfsdk:"redirect"` ActionDuration types.String `tfsdk:"action_duration"` } @@ -558,6 +591,8 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { 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() { @@ -565,23 +600,31 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { } if mitigate.RateLimit != nil { - keys := make([]types.String, len(mitigate.RateLimit.Keys)) - for i, k := range mitigate.RateLimit.Keys { - keys[i] = types.StringValue(k) - } - m.RateLimit = &RateLimit{ - 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), + // TODO diags + keys, diags := basetypes.NewListValueFrom(context.Background(), types.StringType, mitigate.RateLimit.Keys) + 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 diags.HasError() { + fmt.Println("has error") + fmt.Println(diags) } } if mitigate.Redirect != nil { - m.Redirect = &Redirect{ - Location: types.StringValue(mitigate.Redirect.Location), - Permanent: types.BoolValue(mitigate.Redirect.Permanent), - } + m.Redirect = types.ObjectValueMust( + redirectType.AttrTypes, + map[string]attr.Value{ + "location": types.StringValue(mitigate.Redirect.Location), + "permanent": types.BoolValue(mitigate.Redirect.Permanent), + }, + ) } return m } @@ -592,11 +635,11 @@ type Redirect struct { } type RateLimit struct { - Algo types.String `tfsdk:"algo"` - Window types.Int64 `tfsdk:"window"` - Limit types.Int64 `tfsdk:"limit"` - Keys []types.String `tfsdk:"keys"` - Action types.String `tfsdk:"action"` + 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 { From afb5830cb9550394819ff86044b84e90419fc40a Mon Sep 17 00:00:00 2001 From: sueplex Date: Wed, 4 Sep 2024 13:12:26 -0400 Subject: [PATCH 13/17] pass through errors --- vercel/resource_firewall_config.go | 92 ++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index b78f334f..5e5c9d52 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -482,40 +482,41 @@ func (r *FirewallRule) Conditions() []client.ConditionGroup { return groups } -func (r *FirewallRule) Mitigate() client.Mitigate { +func (r *FirewallRule) Mitigate() (client.Mitigate, error) { mit := client.Mitigate{ Action: r.Action.Action.ValueString(), } - fmt.Println(r) if !r.Action.RateLimit.IsNull() { - fmt.Println("RateLimit is not null") rl := &client.RateLimit{} diags := r.Action.RateLimit.As(context.Background(), rl, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false, }) if diags.HasError() { - fmt.Println("Has error") - fmt.Println(diags) + 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{} - r.Action.Redirect.As(context.Background(), rd, basetypes.ObjectAsOptions{ + 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 + return mit, nil } -func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { +func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) (FirewallRule, error) { + var err error r := FirewallRule{ ID: types.StringValue(rule.ID), Name: types.StringValue(rule.Name), @@ -526,7 +527,10 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { r.Active = ref.Active } - r.Action = fromMitigate(rule.Action.Mitigate, ref.Action) + 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)) @@ -550,7 +554,7 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) FirewallRule { r.Active = ref.Active } - return r + return r, nil } /* @@ -587,7 +591,7 @@ type Mitigate struct { ActionDuration types.String `tfsdk:"action_duration"` } -func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { +func fromMitigate(mitigate client.Mitigate, ref Mitigate) (Mitigate, error) { m := Mitigate{ Action: types.StringValue(mitigate.Action), ActionDuration: types.StringValue(mitigate.ActionDuration), @@ -602,6 +606,9 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { if mitigate.RateLimit != nil { // TODO diags 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{ @@ -612,10 +619,6 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { "action": types.StringValue(mitigate.RateLimit.Action), }, ) - if diags.HasError() { - fmt.Println("has error") - fmt.Println(diags) - } } if mitigate.Redirect != nil { m.Redirect = types.ObjectValueMust( @@ -626,7 +629,7 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) Mitigate { }, ) } - return m + return m, nil } type Redirect struct { @@ -721,7 +724,8 @@ func fromCoreRuleset(crsRule client.CoreRuleSet, ref *CRSRuleConfig) *CRSRuleCon return c } -func fromClient(conf client.FirewallConfig, state FirewallConfig) FirewallConfig { +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 @@ -743,8 +747,10 @@ func fromClient(conf client.FirewallConfig, state FirewallConfig) FirewallConfig if state.Rules != nil && len(state.Rules.Rules)-1 > i { stateRule = state.Rules.Rules[i] } - rules[i] = fromFirewallRule(rule, stateRule) - + rules[i], err = fromFirewallRule(rule, stateRule) + if err != nil { + return cfg, err + } } cfg.Rules = &FirewallRules{Rules: rules} } @@ -774,10 +780,10 @@ func fromClient(conf client.FirewallConfig, state FirewallConfig) FirewallConfig cfg.ManagedRulesets.OWASP = fromCRS(conf.CRS, state.ManagedRulesets) } - return cfg + return cfg, nil } -func (f *FirewallConfig) toClient() client.FirewallConfig { +func (f *FirewallConfig) toClient() (client.FirewallConfig, error) { conf := client.FirewallConfig{ ProjectID: f.ProjectID.ValueString(), TeamID: f.TeamID.ValueString(), @@ -803,6 +809,10 @@ func (f *FirewallConfig) toClient() client.FirewallConfig { } 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(), @@ -810,7 +820,7 @@ func (f *FirewallConfig) toClient() client.FirewallConfig { Active: rule.Active.IsNull() || rule.Active.ValueBool(), ConditionGroup: rule.Conditions(), Action: client.Action{ - Mitigate: rule.Mitigate(), + Mitigate: mit, }, }) } @@ -827,7 +837,7 @@ func (f *FirewallConfig) toClient() client.FirewallConfig { }) } } - return conf + return conf, nil } func (r *firewallConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -838,7 +848,11 @@ func (r *firewallConfigResource) Create(ctx context.Context, req resource.Create return } - conf := plan.toClient() + 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 { @@ -849,7 +863,11 @@ func (r *firewallConfigResource) Create(ctx context.Context, req resource.Create if resp.Diagnostics.HasError() { return } - cfg := fromClient(out, plan) + 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...) } @@ -870,7 +888,11 @@ func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequ resp.State.RemoveResource(ctx) return } - cfg := fromClient(out, state) + 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...) } @@ -882,7 +904,11 @@ func (r *firewallConfigResource) Update(ctx context.Context, req resource.Update return } - conf := plan.toClient() + 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 { @@ -893,7 +919,11 @@ func (r *firewallConfigResource) Update(ctx context.Context, req resource.Update if resp.Diagnostics.HasError() { return } - cfg := fromClient(out, plan) + 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...) } @@ -934,10 +964,14 @@ func (r *firewallConfigResource) ImportState(ctx context.Context, req resource.I resp.Diagnostics.AddError("Error importing Firewall Config", err.Error()) return } - conf := fromClient(out, FirewallConfig{ + 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(), From 661a3ce9b730a213192dca8f1e8705322ded3ab9 Mon Sep 17 00:00:00 2001 From: sueplex Date: Wed, 4 Sep 2024 23:42:38 -0400 Subject: [PATCH 14/17] add examples, fix descriptions update docs --- docs/resources/firewall_config.md | 180 ++++++++++++++++-- .../vercel_firewall_config/import.sh | 1 + .../vercel_firewall_config/resource.tf | 141 ++++++++++++++ vercel/resource_firewall_config.go | 41 ++-- 4 files changed, 336 insertions(+), 27 deletions(-) create mode 100644 examples/resources/vercel_firewall_config/import.sh create mode 100644 examples/resources/vercel_firewall_config/resource.tf diff --git a/docs/resources/firewall_config.md b/docs/resources/firewall_config.md index 341975f3..cdb0d39d 100644 --- a/docs/resources/firewall_config.md +++ b/docs/resources/firewall_config.md @@ -10,7 +10,151 @@ description: |- 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 @@ -65,16 +209,16 @@ Optional: Optional: -- `gen` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--gen)) -- `java` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--java)) -- `lfi` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--lfi)) -- `ma` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--ma)) -- `php` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--php)) -- `rce` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rce)) -- `rfi` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--rfi)) -- `sd` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sd)) -- `sqli` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--sqli)) -- `xss` (Attributes) (see [below for nested schema](#nestedatt--managed_rulesets--owasp--xss)) +- `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` @@ -216,7 +360,7 @@ Required: Optional: -- `active` (Boolean) Whether the rule is active or not +- `active` (Boolean) Rule is active or disabled - `description` (String) Read-Only: @@ -241,7 +385,7 @@ Optional: Required: -- `action` (String) Action taken when rate limit is exceeded +- `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 @@ -272,9 +416,17 @@ Required: - `op` (String) How to comparse type to value - `type` (String) Request key type to match against -- `value` (String) 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/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 5e5c9d52..5b74fa51 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -48,7 +48,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Description: "Enable the owasp managed rulesets and select ruleset behaviors", Attributes: map[string]schema.Attribute{ "xss": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Cross Site Scripting Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -59,7 +60,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "sqli": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "SQL Injection Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -70,7 +72,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "lfi": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Local File Inclusion Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -81,7 +84,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "rfi": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Remote File Inclusion Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -92,7 +96,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "rce": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Remote Code Execution Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -103,7 +108,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "sd": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Scanner Detection Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -114,7 +120,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "ma": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Multipart Rules", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -125,7 +132,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "php": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "PHP Attack Detection", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -136,7 +144,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "gen": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Generic Attack Detection", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -147,7 +156,8 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "java": schema.SingleNestedAttribute{ - Optional: true, + Optional: true, + Description: "Java Attack Detection", Attributes: map[string]schema.Attribute{ "active": schema.BoolAttribute{ Optional: true, @@ -187,7 +197,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge }, }, "active": schema.BoolAttribute{ - Description: "Whether the rule is active or not", + Description: "Rule is active or disabled", Optional: true, }, "action": schema.SingleNestedAttribute{ @@ -223,7 +233,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge ElementType: types.StringType, }, "action": schema.StringAttribute{ - Description: "Action taken when rate limit is exceeded", + Description: "Action to take when rate limit is exceeded", Required: true, Validators: []validator.String{ stringvalidator.OneOf("bypass", "log", "challenge", "deny", "rate_limit"), @@ -317,7 +327,7 @@ Define Custom Rules to shape the way your traffic is handled by the Vercel Edge Optional: true, }, "value": schema.StringAttribute{ - Required: true, + Optional: true, }, }, }, @@ -669,8 +679,13 @@ func fromCondition(condition client.Condition, ref Condition) Condition { 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 } From d3e779164ab5b446247d140671edd03dd6b7a2d7 Mon Sep 17 00:00:00 2001 From: sueplex Date: Wed, 4 Sep 2024 23:44:43 -0400 Subject: [PATCH 15/17] some missing returns --- vercel/resource_firewall_config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 5b74fa51..371f8612 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -614,7 +614,6 @@ func fromMitigate(mitigate client.Mitigate, ref Mitigate) (Mitigate, error) { } if mitigate.RateLimit != nil { - // TODO diags 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()) @@ -872,6 +871,7 @@ func (r *firewallConfigResource) Create(ctx context.Context, req resource.Create out, err := r.client.PutFirewallConfig(ctx, conf) if err != nil { diags.AddError("failed to create firewall config", err.Error()) + return } resp.Diagnostics.Append(diags...) @@ -898,6 +898,7 @@ func (r *firewallConfigResource) Read(ctx context.Context, req resource.ReadRequ 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) @@ -928,6 +929,7 @@ func (r *firewallConfigResource) Update(ctx context.Context, req resource.Update out, err := r.client.PutFirewallConfig(ctx, conf) if err != nil { diags.AddError("failed to create firewall config", err.Error()) + return } resp.Diagnostics.Append(diags...) @@ -959,6 +961,7 @@ func (r *firewallConfigResource) Delete(ctx context.Context, req resource.Delete _, 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(), From dd4289e3e87c3e7c1529ddb7846d8a421dd9de9b Mon Sep 17 00:00:00 2001 From: sueplex Date: Wed, 4 Sep 2024 23:52:23 -0400 Subject: [PATCH 16/17] uncommit comments --- vercel/provider_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/provider_test.go b/vercel/provider_test.go index d1ec38c0..c5c98b00 100644 --- a/vercel/provider_test.go +++ b/vercel/provider_test.go @@ -17,7 +17,7 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe func mustHaveEnv(t *testing.T, name string) { if os.Getenv(name) == "" { - //t.Fatalf("%s environment variable must be set for acceptance tests", name) + t.Fatalf("%s environment variable must be set for acceptance tests", name) } } From bcff204ffdf6971a5eb62c0f0088dff51a7684e9 Mon Sep 17 00:00:00 2001 From: sueplex Date: Fri, 6 Sep 2024 09:42:59 -0400 Subject: [PATCH 17/17] remove commented code --- client/firewall_config.go | 2 -- vercel/resource_firewall_config.go | 8 -------- 2 files changed, 10 deletions(-) diff --git a/client/firewall_config.go b/client/firewall_config.go index 4ebfbc89..5b7ee8c2 100644 --- a/client/firewall_config.go +++ b/client/firewall_config.go @@ -109,8 +109,6 @@ func (c *Client) PutFirewallConfig(ctx context.Context, cfg FirewallConfig) (Fir } payload := mustMarshal(cfg) - //fmt.Println("Payload: ", string(payload)) - err := c.doRequest(clientRequest{ ctx: ctx, method: "PUT", diff --git a/vercel/resource_firewall_config.go b/vercel/resource_firewall_config.go index 371f8612..b0e2c71e 100644 --- a/vercel/resource_firewall_config.go +++ b/vercel/resource_firewall_config.go @@ -567,14 +567,6 @@ func fromFirewallRule(rule client.FirewallRule, ref FirewallRule) (FirewallRule, return r, nil } -/* - type Mitigate struct { - Action types.String `tfsdk:"action"` - RateLimit *RateLimit `tfsdk:"rate_limit"` - Redirect *Redirect `tfsdk:"redirect"` - ActionDuration types.String `tfsdk:"action_duration"` - } -*/ var redirectType = types.ObjectType{ AttrTypes: map[string]attr.Type{ "location": types.StringType,