From 7ef9a81ea968b8a4adaf96204821d6fd16bfa8a9 Mon Sep 17 00:00:00 2001 From: Inanna Malick Date: Tue, 14 May 2024 08:42:01 -0700 Subject: [PATCH] Adds support for configuring zt risk behavior via terraform --- .changelog/3307.txt | 4 + internal/framework/provider/provider.go | 2 + .../framework/service/risk_behavior/model.go | 14 ++ .../service/risk_behavior/resource.go | 203 ++++++++++++++++++ .../service/risk_behavior/resource_test.go | 89 ++++++++ .../framework/service/risk_behavior/schema.go | 65 ++++++ 6 files changed, 377 insertions(+) create mode 100644 .changelog/3307.txt create mode 100644 internal/framework/service/risk_behavior/model.go create mode 100644 internal/framework/service/risk_behavior/resource.go create mode 100644 internal/framework/service/risk_behavior/resource_test.go create mode 100644 internal/framework/service/risk_behavior/schema.go diff --git a/.changelog/3307.txt b/.changelog/3307.txt new file mode 100644 index 00000000000..26bedf439ef --- /dev/null +++ b/.changelog/3307.txt @@ -0,0 +1,4 @@ +```release-note:new-resource +cloudflare_risk_behavior +``` + diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index ead4ce424d6..d2ba5db2af3 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -24,6 +24,7 @@ import ( "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list_item" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/origin_ca_certificate" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/r2_bucket" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/risk_behavior" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/rulesets" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/turnstile" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/user" @@ -357,6 +358,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re hyperdrive_config.NewResource, list_item.NewResource, r2_bucket.NewResource, + risk_behavior.NewResource, rulesets.NewResource, turnstile.NewResource, access_mutual_tls_hostname_settings.NewResource, diff --git a/internal/framework/service/risk_behavior/model.go b/internal/framework/service/risk_behavior/model.go new file mode 100644 index 00000000000..cb51257b312 --- /dev/null +++ b/internal/framework/service/risk_behavior/model.go @@ -0,0 +1,14 @@ +package risk_behavior + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type RiskBehaviorModel struct { + AccountID types.String `tfsdk:"account_id"` + Behaviors []RiskBehaviorBehaviorModel `tfsdk:"behavior"` +} + +type RiskBehaviorBehaviorModel struct { + Enabled types.Bool `tfsdk:"enabled"` + Name types.String `tfsdk:"name"` + RiskLevel types.String `tfsdk:"risk_level"` +} diff --git a/internal/framework/service/risk_behavior/resource.go b/internal/framework/service/risk_behavior/resource.go new file mode 100644 index 00000000000..94547429358 --- /dev/null +++ b/internal/framework/service/risk_behavior/resource.go @@ -0,0 +1,203 @@ +package risk_behavior + +import ( + "context" + "fmt" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &RiskBehaviorResource{} + +func NewResource() resource.Resource { + return &RiskBehaviorResource{} +} + +// RiskBehaviorResource defines the resource implementation. +type RiskBehaviorResource struct { + client *muxclient.Client +} + +func (r *RiskBehaviorResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_risk_behavior" +} + +func (r *RiskBehaviorResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*muxclient.Client) + + if !ok { + resp.Diagnostics.AddError( + "unexpected resource configure type", + fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *RiskBehaviorResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *RiskBehaviorModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + accountId := data.AccountID.ValueString() + + behaviorsMap, err := ConvertBehaviorsTtoC(data.Behaviors) + if err != nil { + resp.Diagnostics.AddError("invalid risk level", err.Error()) + return + } + + behaviors, err := r.client.V1.UpdateBehaviors(ctx, accountId, + cloudflare.Behaviors{Behaviors: behaviorsMap}, + ) + if err != nil { + resp.Diagnostics.AddError("failed to create risk behaviors", err.Error()) + return + } + + behaviorsSet := ConvertBehaviorsCtoT(behaviors.Behaviors) + + data.AccountID = types.StringValue(accountId) + data.Behaviors = behaviorsSet + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RiskBehaviorResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *RiskBehaviorModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + accountId := data.AccountID.ValueString() + + behaviors, err := r.client.V1.Behaviors(ctx, accountId) + if err != nil { + resp.Diagnostics.AddError("failed reading risk behaviors", err.Error()) + return + } + + behaviorsSet := ConvertBehaviorsCtoT(behaviors.Behaviors) + + data.AccountID = types.StringValue(accountId) + data.Behaviors = behaviorsSet + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func ConvertBehaviorsTtoC(b []RiskBehaviorBehaviorModel) (map[string]cloudflare.Behavior, error) { + + behaviorsMap := map[string]cloudflare.Behavior{} + for _, b := range b { + riskLevel, err := cloudflare.RiskLevelFromString(b.RiskLevel.ValueString()) + if err != nil { + return nil, err + } + + enabled := b.Enabled.ValueBool() + + behavior := cloudflare.Behavior{ + Enabled: &enabled, + RiskLevel: *riskLevel, + } + + behaviorsMap[b.Name.ValueString()] = behavior + } + + return behaviorsMap, nil +} + +func ConvertBehaviorsCtoT(b map[string]cloudflare.Behavior) []RiskBehaviorBehaviorModel { + + behaviorsSet := []RiskBehaviorBehaviorModel{} + + for k, b := range b { + behavior := RiskBehaviorBehaviorModel{ + Enabled: types.BoolPointerValue(b.Enabled), + Name: types.StringValue(k), + RiskLevel: types.StringValue(fmt.Sprint(b.RiskLevel)), + } + + behaviorsSet = append(behaviorsSet, behavior) + } + + return behaviorsSet +} + +func (r *RiskBehaviorResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *RiskBehaviorModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + accountId := data.AccountID.ValueString() + + behaviorsMap, err := ConvertBehaviorsTtoC(data.Behaviors) + if err != nil { + resp.Diagnostics.AddError("invalid risk level", err.Error()) + return + } + + behaviors, err := r.client.V1.UpdateBehaviors(ctx, accountId, + cloudflare.Behaviors{Behaviors: behaviorsMap}, + ) + if err != nil { + resp.Diagnostics.AddError("failed to update risk behaviors", err.Error()) + return + } + + behaviorsSet := ConvertBehaviorsCtoT(behaviors.Behaviors) + + data.AccountID = types.StringValue(accountId) + data.Behaviors = behaviorsSet + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RiskBehaviorResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *RiskBehaviorModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // tflog.Debug(ctx, "Resetting all zero trust risk behaviors to enabled: false, risk_level: low") + + behaviors, err := r.client.V1.Behaviors(ctx, data.AccountID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("failed to get risk behaviors", err.Error()) + return + } + + // set all risk behavior values to false/low before running update + for _, behavior := range behaviors.Behaviors { + behavior.Enabled = cloudflare.BoolPtr(false) + behavior.RiskLevel = cloudflare.Low + } + + _, err = r.client.V1.UpdateBehaviors(ctx, data.AccountID.ValueString(), behaviors) + if err != nil { + resp.Diagnostics.AddError("failed to reset risk behaviors", err.Error()) + return + } +} diff --git a/internal/framework/service/risk_behavior/resource_test.go b/internal/framework/service/risk_behavior/resource_test.go new file mode 100644 index 00000000000..da05af51b3b --- /dev/null +++ b/internal/framework/service/risk_behavior/resource_test.go @@ -0,0 +1,89 @@ +package risk_behavior_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +func init() { + resource.AddTestSweepers("cloudflare_risk_behavior", &resource.Sweeper{ + Name: "cloudflare_risk_behavior", + F: func(region string) error { + client, err := acctest.SharedV1Client() + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + if err != nil { + return fmt.Errorf("error establishing client: %w", err) + } + + ctx := context.Background() + + behaviors, err := client.Behaviors(ctx, accountID) + if err != nil { + return fmt.Errorf("failed to get risk behaviors: %w", err) + } + + // set all risk behavior values to false/low before running update + for _, behavior := range behaviors.Behaviors { + behavior.Enabled = cloudflare.BoolPtr(false) + behavior.RiskLevel = cloudflare.Low + } + + _, err = client.UpdateBehaviors(ctx, accountID, behaviors) + if err != nil { + return fmt.Errorf("failed to reset risk behaviors: %w", err) + } + + return nil + }, + }) +} + +func TestAccCloudflareRiskBehavior_Basic(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := "cloudflare_risk_behavior." + rnd + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareRiskBehaviors(rnd, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), + resource.TestCheckResourceAttr(name, "behavior.#", "2"), + ), + }, + }, + }) +} + +func testAccCloudflareRiskBehaviors(name, accountId string) string { + return fmt.Sprintf(` + resource cloudflare_risk_behavior %s { + account_id = "%s" + behavior { + name = "imp_travel" + enabled = true + risk_level = "high" + } + behavior { + name = "high_dlp" + enabled = true + risk_level = "medium" + } + }`, name, accountId) +} diff --git a/internal/framework/service/risk_behavior/schema.go b/internal/framework/service/risk_behavior/schema.go new file mode 100644 index 00000000000..f2856c00f04 --- /dev/null +++ b/internal/framework/service/risk_behavior/schema.go @@ -0,0 +1,65 @@ +package risk_behavior + +import ( + "context" + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func (r *RiskBehaviorResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: heredoc.Doc(` + The [Risk Behavior](https://developers.cloudflare.com/cloudflare-one/insights/risk-score/) resource allows you to configure Cloudflare Risk Behaviors for an account. + `), + + Attributes: map[string]schema.Attribute{ + consts.AccountIDSchemaKey: schema.StringAttribute{ + MarkdownDescription: consts.AccountIDSchemaDescription, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + + Blocks: map[string]schema.Block{ + + "behavior": schema.SetNestedBlock{ + MarkdownDescription: "Zero Trust risk behaviors configured on this account", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Name of this risk behavior type", + }, + "enabled": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether this risk behavior type is enabled.", + }, + "risk_level": schema.StringAttribute{ + Required: true, + MarkdownDescription: fmt.Sprintf("Risk level. %s", utils.RenderAvailableDocumentationValuesStringSlice([]string{"low", "medium", "high"})), + Validators: []validator.String{ + stringvalidator.OneOf("low", "medium", "high"), + }, + }, + }, + }, + }, + }, + } +}