Skip to content

Commit

Permalink
Adds support for configuring zt risk behavior via terraform
Browse files Browse the repository at this point in the history
  • Loading branch information
Inanna Malick committed May 16, 2024
1 parent 815f78e commit 7ef9a81
Show file tree
Hide file tree
Showing 6 changed files with 377 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .changelog/3307.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```release-note:new-resource
cloudflare_risk_behavior
```

2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions internal/framework/service/risk_behavior/model.go
Original file line number Diff line number Diff line change
@@ -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"`
}
203 changes: 203 additions & 0 deletions internal/framework/service/risk_behavior/resource.go
Original file line number Diff line number Diff line change
@@ -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
}
}
89 changes: 89 additions & 0 deletions internal/framework/service/risk_behavior/resource_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
65 changes: 65 additions & 0 deletions internal/framework/service/risk_behavior/schema.go
Original file line number Diff line number Diff line change
@@ -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"),
},
},
},
},
},
},
}
}

0 comments on commit 7ef9a81

Please sign in to comment.