diff --git a/examples/project/main.tf b/examples/project/main.tf index 3328143..43d56e1 100644 --- a/examples/project/main.tf +++ b/examples/project/main.tf @@ -8,7 +8,7 @@ terraform { } resource "quant_project" "tf" { - name = "Terraform project" + name = "Terraform project 1" allow_query_params = false basic_auth_username = "quant" basic_auth_password = "qcdn" diff --git a/examples/rules/main.tf b/examples/rules/main.tf index 066345f..a2f7d6d 100644 --- a/examples/rules/main.tf +++ b/examples/rules/main.tf @@ -33,6 +33,16 @@ output "header_rule_result" { value = quant_rule_headers.header_rule.uuid } +resource "quant_rule_custom_response" "custom_response" { + project = "api-test" + name = "Terraform custom response rule" + disabled = false + domain = "any" + url = "/test-response" + + custom_response_status_code = 200 + custom_response_body = "

This is a custom response

" +} # resource "quant_rule_proxy" "proxy_rule" { # project = "api-test" diff --git a/internal/provider/rule_auth_resource.go b/internal/provider/rule_auth_resource.go index 0231755..5f4668a 100644 --- a/internal/provider/rule_auth_resource.go +++ b/internal/provider/rule_auth_resource.go @@ -9,7 +9,9 @@ import ( "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" openapi "github.com/quantcdn/quant-admin-go" ) @@ -80,6 +82,13 @@ func (r *ruleAuth) Metadata(_ context.Context, req resource.MetadataRequest, res func (r *ruleAuth) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "The rules UUID", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "name": schema.StringAttribute{ MarkdownDescription: "A name for the rule", Optional: true, @@ -91,8 +100,8 @@ func (r *ruleAuth) Schema(_ context.Context, _ resource.SchemaRequest, resp *res "disabled": schema.BoolAttribute{ MarkdownDescription: "If this rule is disabled", Optional: true, - Default: booldefault.StaticBool(false), Computed: true, + Default: booldefault.StaticBool(false), }, "domain": schema.StringAttribute{ MarkdownDescription: "The domain the rule applies to", @@ -100,6 +109,12 @@ func (r *ruleAuth) Schema(_ context.Context, _ resource.SchemaRequest, resp *res Computed: true, Default: stringdefault.StaticString("any"), }, + "url": schema.StringAttribute{ + MarkdownDescription: "The URL to apply to", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("/*"), + }, "countries": schema.ListAttribute{ MarkdownDescription: "A list of countries", Optional: true, @@ -161,6 +176,27 @@ func (r *ruleAuth) Create(ctx context.Context, req resource.CreateRequest, resp rule := openapi.NewRuleAuthRequest() + if plan.Url.IsNull() { + plan.Url = types.StringValue("*") + } + + if plan.CountryInclude.IsNull() { + rule.SetCountry("any") + } + + if plan.MethodInclude.IsNull() { + rule.SetMethod("any") + } + + if plan.IpInclude.IsNull() { + rule.SetIp("any") + } + + rule.SetName(plan.Name.ValueString()) + rule.SetDisabled(plan.Disabled.ValueBool()) + rule.SetDomain(plan.Domain.ValueString()) + rule.SetUrl(plan.Url.ValueString()) + if !plan.CountryInclude.IsNull() && !plan.CountryInclude.IsUnknown() { if plan.CountryInclude.ValueBool() { rule.SetCountryIs(helpers.FlattenToStrings(plan.Countries)) @@ -185,8 +221,7 @@ func (r *ruleAuth) Create(ctx context.Context, req resource.CreateRequest, resp } } - rule.SetName(plan.Name.ValueString()) - rule.SetDomain(plan.Domain.ValueString()) + // Rule behaviour. rule.SetAuthUser(plan.AuthUser.ValueString()) rule.SetAuthPass(plan.AuthUser.ValueString()) @@ -257,6 +292,27 @@ func (r *ruleAuth) Update(ctx context.Context, req resource.UpdateRequest, resp rule := openapi.NewRuleAuthRequest() + if plan.Url.IsNull() { + plan.Url = types.StringValue("*") + } + + if plan.CountryInclude.IsNull() { + rule.SetCountry("any") + } + + if plan.MethodInclude.IsNull() { + rule.SetMethod("any") + } + + if plan.IpInclude.IsNull() { + rule.SetIp("any") + } + + rule.SetName(plan.Name.ValueString()) + rule.SetDisabled(plan.Disabled.ValueBool()) + rule.SetDomain(plan.Domain.ValueString()) + rule.SetUrl(plan.Url.ValueString()) + if !plan.CountryInclude.IsNull() && !plan.CountryInclude.IsUnknown() { if plan.CountryInclude.ValueBool() { rule.SetCountryIs(helpers.FlattenToStrings(plan.Countries)) @@ -281,8 +337,7 @@ func (r *ruleAuth) Update(ctx context.Context, req resource.UpdateRequest, resp } } - rule.SetName(plan.Name.ValueString()) - rule.SetDomain(plan.Domain.ValueString()) + // Rule behaviour. rule.SetAuthUser(plan.AuthUser.ValueString()) rule.SetAuthPass(plan.AuthUser.ValueString()) diff --git a/internal/provider/rule_custom_response_resource.go b/internal/provider/rule_custom_response_resource.go index 76b46fe..4f3ebd5 100644 --- a/internal/provider/rule_custom_response_resource.go +++ b/internal/provider/rule_custom_response_resource.go @@ -85,6 +85,13 @@ func (r *ruleCustomResponse) Metadata(_ context.Context, req resource.MetadataRe func (r *ruleCustomResponse) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "The rules UUID", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "name": schema.StringAttribute{ MarkdownDescription: "A name for the rule", Optional: true, diff --git a/internal/provider/rule_proxy_resource.go b/internal/provider/rule_proxy_resource.go index 7006f14..ed9d74b 100644 --- a/internal/provider/rule_proxy_resource.go +++ b/internal/provider/rule_proxy_resource.go @@ -10,7 +10,9 @@ import ( "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" openapi "github.com/quantcdn/quant-admin-go" ) @@ -54,7 +56,7 @@ type ruleProxyModel struct { Host types.String `tfsdk:"host"` AuthUser types.String `tfsdk:"auth_user"` AuthPass types.String `tfsdk:"auth_pass"` - DisableSSLVerify types.String `tfsdk:"disable_ssl_verify"` + DisableSSLVerify types.Bool `tfsdk:"disable_ssl_verify"` CacheLifetime types.Int64 `tfsdk:"cache_lifetime"` Only404 types.Bool `tfsdk:"only_404"` StripHeaders []types.String `tfsdk:"strip_headers"` @@ -111,6 +113,13 @@ func (r *ruleProxy) Metadata(_ context.Context, req resource.MetadataRequest, re func (r *ruleProxy) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "The rules UUID", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "name": schema.StringAttribute{ MarkdownDescription: "A name for the rule", Optional: true, @@ -122,14 +131,20 @@ func (r *ruleProxy) Schema(_ context.Context, _ resource.SchemaRequest, resp *re "disabled": schema.BoolAttribute{ MarkdownDescription: "If this rule is disabled", Optional: true, - Default: booldefault.StaticBool(false), Computed: true, + Default: booldefault.StaticBool(false), }, "domain": schema.StringAttribute{ MarkdownDescription: "The domain the rule applies to", Optional: true, + Computed: true, Default: stringdefault.StaticString("any"), + }, + "url": schema.StringAttribute{ + MarkdownDescription: "The URL to apply to", + Optional: true, Computed: true, + Default: stringdefault.StaticString("*"), }, "countries": schema.ListAttribute{ MarkdownDescription: "A list of countries", @@ -164,11 +179,6 @@ func (r *ruleProxy) Schema(_ context.Context, _ resource.SchemaRequest, resp *re }, // Rule behaviours - "url": schema.StringAttribute{ - MarkdownDescription: "The URL pattern to apply the rule to", - Computed: true, - Default: stringdefault.StaticString("*"), - }, "to": schema.StringAttribute{ MarkdownDescription: "The origin hostname to proxy to", Required: true, @@ -309,61 +319,80 @@ func (r *ruleProxy) Create(ctx context.Context, req resource.CreateRequest, resp ) } - proxy := openapi.NewRuleProxyRequest() + rule := openapi.NewRuleProxyRequest() + + if plan.Url.IsNull() { + plan.Url = types.StringValue("*") + } + + if plan.CountryInclude.IsNull() { + rule.SetCountry("any") + } + + if plan.MethodInclude.IsNull() { + rule.SetMethod("any") + } + + if plan.IpInclude.IsNull() { + rule.SetIp("any") + } - proxy.SetName(plan.Name.ValueString()) - proxy.SetDisabled(plan.Disabled.ValueBool()) - proxy.SetDomain(plan.Domain.ValueString()) + rule.SetName(plan.Name.ValueString()) + rule.SetDisabled(plan.Disabled.ValueBool()) + rule.SetDomain(plan.Domain.ValueString()) + rule.SetUrl(plan.Url.ValueString()) if !plan.CountryInclude.IsNull() && !plan.CountryInclude.IsUnknown() { if plan.CountryInclude.ValueBool() { - proxy.SetCountry("country_is") - proxy.SetCountryIs(helpers.FlattenToStrings(plan.Countries)) + rule.SetCountryIs(helpers.FlattenToStrings(plan.Countries)) } else { - proxy.SetCountry("country_is_not") - proxy.SetCountryIsNot(helpers.FlattenToStrings(plan.Countries)) + rule.SetCountryIsNot(helpers.FlattenToStrings(plan.Countries)) } } if !plan.MethodInclude.IsNull() && !plan.MethodInclude.IsUnknown() { if plan.MethodInclude.ValueBool() { - proxy.SetMethod("method_is") - proxy.SetMethodIs(helpers.FlattenToStrings(plan.Methods)) + rule.SetMethodIs(helpers.FlattenToStrings(plan.Methods)) } else { - proxy.SetMethod("method_is_not") - proxy.SetMethodIsNot(helpers.FlattenToStrings(plan.Methods)) + rule.SetMethodIsNot(helpers.FlattenToStrings(plan.Methods)) } } if !plan.IpInclude.IsNull() && !plan.IpInclude.IsUnknown() { if plan.IpInclude.ValueBool() { - proxy.SetIp("ip_is") - proxy.SetIpIs(helpers.FlattenToStrings(plan.Ips)) + rule.SetIpIs(helpers.FlattenToStrings(plan.Ips)) } else { - proxy.SetIp("ip_is_not") - proxy.SetIpIsNot(helpers.FlattenToStrings(plan.Ips)) + rule.SetIpIsNot(helpers.FlattenToStrings(plan.Ips)) } } // Rule behaviour. - proxy.SetUrl(plan.Url.ValueString()) - proxy.SetTo(plan.To.ValueString()) - proxy.SetHost(plan.Host.ValueString()) - proxy.SetAuthUser(plan.AuthUser.ValueString()) - proxy.SetAuthPass(plan.AuthPass.ValueString()) - // proxy.SetDisableSslVerify(plan.DisableSSLVerify.BoolValue()) - proxy.SetCacheLifetime(int32(plan.CacheLifetime.ValueInt64())) - proxy.SetOnlyProxy404(plan.Only404.ValueBool()) - proxy.SetStripHeaders(helpers.FlattenToStrings(plan.StripHeaders)) - proxy.SetWafEnabled(plan.WafEnable.ValueBool()) + rule.SetTo(plan.To.ValueString()) + rule.SetHost(plan.Host.ValueString()) + rule.SetAuthUser(plan.AuthUser.ValueString()) + rule.SetAuthPass(plan.AuthPass.ValueString()) + rule.SetDisableSslVerify(plan.DisableSSLVerify.ValueBool()) + rule.SetCacheLifetime(int32(plan.CacheLifetime.ValueInt64())) + rule.SetOnlyProxy404(plan.Only404.ValueBool()) + rule.SetStripHeaders(helpers.FlattenToStrings(plan.StripHeaders)) + rule.SetWafEnabled(plan.WafEnable.ValueBool()) var wafConfig openapi.RuleProxyRequestWafConfig wafConfig.SetMode(plan.WafConfig.Mode.ValueString()) wafConfig.SetParanoiaLevel(int32(plan.WafConfig.ParanoiaLevel.ValueInt64())) // @todo: Update client — IPs should probably be strings. - // wafConfig.SetAllowIp(helpers.FlattenToInt32(plan.WafConfig.AllowIp)) - // wafConfig.SetBlockIp(helpers.FlattenToInt32(plan.WafConfig.BlockIp)) + var ips []string + for _, v := range plan.WafConfig.AllowIp { + ips = append(ips, v.ValueString()) + } + wafConfig.SetAllowIp(ips) + + var blockIps []string + for _, v := range plan.WafConfig.BlockIp { + blockIps = append(blockIps, v.ValueString()) + } + wafConfig.SetBlockIp(blockIps) wafConfig.SetAllowRules(helpers.FlattenToInt32(plan.WafConfig.AllowRules)) wafConfig.SetBlockReferer(helpers.FlattenToStrings(plan.WafConfig.BlockReferer)) @@ -371,13 +400,13 @@ func (r *ruleProxy) Create(ctx context.Context, req resource.CreateRequest, resp wafConfig.SetNotifySlack(plan.WafConfig.NotifySlack.ValueString()) wafConfig.SetNotifySlackHitsRpm(int32(plan.WafConfig.NotifySlackHitsRpm.ValueInt64())) - proxy.SetWafConfig(wafConfig) + rule.SetWafConfig(wafConfig) organization := r.client.Organization project := plan.Project.ValueString() client := r.client.Admin.RulesAPI - res, _, err := client.OrganizationsOrganizationProjectsProjectRulesProxyPost(context.Background(), organization, project).RuleProxyRequest(*proxy).Execute() + res, _, err := client.OrganizationsOrganizationProjectsProjectRulesProxyPost(context.Background(), organization, project).RuleProxyRequest(*rule).Execute() if err != nil { resp.Diagnostics.AddError( @@ -435,61 +464,56 @@ func (r *ruleProxy) Update(ctx context.Context, req resource.UpdateRequest, resp if resp.Diagnostics.HasError() { return } - proxy := openapi.NewRuleProxyRequest() + rule := openapi.NewRuleProxyRequest() - proxy.SetName(plan.Name.ValueString()) - proxy.SetDisabled(plan.Disabled.ValueBool()) - proxy.SetDomain(plan.Domain.ValueString()) + if plan.Url.IsNull() { + plan.Url = types.StringValue("*") + } - if !plan.CountryInclude.IsNull() && !plan.CountryInclude.IsUnknown() { - if plan.CountryInclude.ValueBool() { - proxy.SetCountry("country_is") - proxy.SetCountryIs(helpers.FlattenToStrings(plan.Countries)) - } else { - proxy.SetCountry("country_is_not") - proxy.SetCountryIsNot(helpers.FlattenToStrings(plan.Countries)) - } + if plan.CountryInclude.IsNull() { + rule.SetCountry("any") } - if !plan.MethodInclude.IsNull() && !plan.MethodInclude.IsUnknown() { - if plan.MethodInclude.ValueBool() { - proxy.SetMethod("method_is") - proxy.SetMethodIs(helpers.FlattenToStrings(plan.Methods)) - } else { - proxy.SetMethod("method_is_not") - proxy.SetMethodIsNot(helpers.FlattenToStrings(plan.Methods)) - } + if plan.MethodInclude.IsNull() { + rule.SetMethod("any") } - if !plan.IpInclude.IsNull() && !plan.IpInclude.IsUnknown() { - if plan.IpInclude.ValueBool() { - proxy.SetIp("ip_is") - proxy.SetIpIs(helpers.FlattenToStrings(plan.Ips)) - } else { - proxy.SetIp("ip_is_not") - proxy.SetIpIsNot(helpers.FlattenToStrings(plan.Ips)) - } + if plan.IpInclude.IsNull() { + rule.SetIp("any") } + rule.SetName(plan.Name.ValueString()) + rule.SetDisabled(plan.Disabled.ValueBool()) + rule.SetDomain(plan.Domain.ValueString()) + rule.SetUrl(plan.Url.ValueString()) + // Rule behaviour. - proxy.SetUrl(plan.Url.ValueString()) - proxy.SetTo(plan.To.ValueString()) - proxy.SetHost(plan.Host.ValueString()) - proxy.SetAuthUser(plan.AuthUser.ValueString()) - proxy.SetAuthPass(plan.AuthPass.ValueString()) - // proxy.SetDisableSslVerify(plan.DisableSSLVerify.BoolValue()) - proxy.SetCacheLifetime(int32(plan.CacheLifetime.ValueInt64())) - proxy.SetOnlyProxy404(plan.Only404.ValueBool()) - proxy.SetStripHeaders(helpers.FlattenToStrings(plan.StripHeaders)) - proxy.SetWafEnabled(plan.WafEnable.ValueBool()) + rule.SetTo(plan.To.ValueString()) + rule.SetHost(plan.Host.ValueString()) + rule.SetAuthUser(plan.AuthUser.ValueString()) + rule.SetAuthPass(plan.AuthPass.ValueString()) + rule.SetDisableSslVerify(plan.DisableSSLVerify.ValueBool()) + rule.SetCacheLifetime(int32(plan.CacheLifetime.ValueInt64())) + rule.SetOnlyProxy404(plan.Only404.ValueBool()) + rule.SetStripHeaders(helpers.FlattenToStrings(plan.StripHeaders)) + rule.SetWafEnabled(plan.WafEnable.ValueBool()) var wafConfig openapi.RuleProxyRequestWafConfig wafConfig.SetMode(plan.WafConfig.Mode.ValueString()) wafConfig.SetParanoiaLevel(int32(plan.WafConfig.ParanoiaLevel.ValueInt64())) // @todo: Update client — IPs should probably be strings. - // wafConfig.SetAllowIp(helpers.FlattenToInt32(plan.WafConfig.AllowIp)) - // wafConfig.SetBlockIp(helpers.FlattenToInt32(plan.WafConfig.BlockIp)) + var ips []string + for _, v := range plan.WafConfig.AllowIp { + ips = append(ips, v.ValueString()) + } + wafConfig.SetAllowIp(ips) + + var blockIps []string + for _, v := range plan.WafConfig.BlockIp { + blockIps = append(blockIps, v.ValueString()) + } + wafConfig.SetBlockIp(blockIps) wafConfig.SetAllowRules(helpers.FlattenToInt32(plan.WafConfig.AllowRules)) wafConfig.SetBlockReferer(helpers.FlattenToStrings(plan.WafConfig.BlockReferer)) @@ -497,13 +521,13 @@ func (r *ruleProxy) Update(ctx context.Context, req resource.UpdateRequest, resp wafConfig.SetNotifySlack(plan.WafConfig.NotifySlack.ValueString()) wafConfig.SetNotifySlackHitsRpm(int32(plan.WafConfig.NotifySlackHitsRpm.ValueInt64())) - proxy.SetWafConfig(wafConfig) + rule.SetWafConfig(wafConfig) organization := r.client.Organization project := plan.Project.ValueString() client := r.client.Admin.RulesAPI - _, _, err := client.OrganizationsOrganizationProjectsProjectRulesProxyRulePatch(context.Background(), organization, project, plan.Uuid.ValueString()).RuleProxyRequest(*proxy).Execute() + _, _, err := client.OrganizationsOrganizationProjectsProjectRulesProxyRulePatch(context.Background(), organization, project, plan.Uuid.ValueString()).RuleProxyRequest(*rule).Execute() if err != nil { resp.Diagnostics.AddError(