diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index afe4903201..57a7e62fcd 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -28,7 +28,7 @@ import ( "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/hyperdrive_config" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/infrastructure_access_target_deprecated" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check" - "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check_rules" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check_rule" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list_item" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/origin_ca_certificate" @@ -390,7 +390,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re infrastructure_access_target_deprecated.NewResource, zero_trust_infrastructure_access_target.NewResource, leaked_credential_check.NewResource, - leaked_credential_check_rules.NewResource, + leaked_credential_check_rule.NewResource, } } diff --git a/internal/framework/service/leaked_credential_check_rules/model.go b/internal/framework/service/leaked_credential_check_rule/model.go similarity index 58% rename from internal/framework/service/leaked_credential_check_rules/model.go rename to internal/framework/service/leaked_credential_check_rule/model.go index 316b8ad51c..f9f8e6192e 100644 --- a/internal/framework/service/leaked_credential_check_rules/model.go +++ b/internal/framework/service/leaked_credential_check_rule/model.go @@ -1,13 +1,9 @@ -package leaked_credential_check_rules +package leaked_credential_check_rule import "github.com/hashicorp/terraform-plugin-framework/types" type LeakedCredentialCheckRulesModel struct { - ZoneID types.String `tfsdk:"zone_id"` - Rules []LCCRuleValueModel `tfsdk:"rule"` -} - -type LCCRuleValueModel struct { + ZoneID types.String `tfsdk:"zone_id"` ID types.String `tfsdk:"id"` Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` diff --git a/internal/framework/service/leaked_credential_check_rule/resource.go b/internal/framework/service/leaked_credential_check_rule/resource.go new file mode 100644 index 0000000000..6f4917d0e1 --- /dev/null +++ b/internal/framework/service/leaked_credential_check_rule/resource.go @@ -0,0 +1,151 @@ +package leaked_credential_check_rule + +import ( + "context" + "fmt" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &LeakedCredentialCheckRuleResource{} + _ resource.ResourceWithImportState = &LeakedCredentialCheckRuleResource{} +) + +func NewResource() resource.Resource { + return &LeakedCredentialCheckRuleResource{} +} + +type LeakedCredentialCheckRuleResource struct { + client *muxclient.Client +} + +func (r *LeakedCredentialCheckRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_leaked_credential_check_rule" +} + +func (r *LeakedCredentialCheckRuleResource) Configure(_ 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 *LeakedCredentialCheckRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LeakedCredentialCheckRulesModel + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + detection, err := r.client.V1.LeakedCredentialCheckCreateDetection(ctx, cloudflare.ZoneIdentifier(data.ZoneID.ValueString()), cloudflare.LeakedCredentialCheckCreateDetectionParams{ + Username: data.Username.ValueString(), + Password: data.Password.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Error creating a user-defined detection patter for Leaked Credential Check", err.Error()) + return + } + + data.ID = types.StringValue(detection.ID) + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r *LeakedCredentialCheckRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LeakedCredentialCheckRulesModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := state.ZoneID.ValueString() + var foundRule cloudflare.LeakedCredentialCheckDetectionEntry + rules, err := r.client.V1.LeakedCredentialCheckListDetections(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.LeakedCredentialCheckListDetectionsParams{}) + if err != nil { + resp.Diagnostics.AddError("Error listing Leaked Credential Check user-defined detection patterns", err.Error()) + return + } + + // leaked credentials doens't offer a single get operation so + // loop until we find the matching ID. + for _, rule := range rules { + if rule.ID == state.ID.ValueString() { + foundRule = rule + break + } + } + + state.Password = types.StringValue(foundRule.Password) + state.Username = types.StringValue(foundRule.Username) + state.ID = types.StringValue(foundRule.ID) + state.ZoneID = types.StringValue(zoneID) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +func (r *LeakedCredentialCheckRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data LeakedCredentialCheckRulesModel + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + zoneID := cloudflare.ZoneIdentifier(data.ZoneID.ValueString()) + _, err := r.client.V1.LeakedCredentialCheckUpdateDetection(ctx, zoneID, cloudflare.LeakedCredentialCheckUpdateDetectionParams{ + LeakedCredentialCheckDetectionEntry: cloudflare.LeakedCredentialCheckDetectionEntry{ + ID: data.ID.ValueString(), + Username: data.Username.ValueString(), + Password: data.Password.ValueString(), + }, + }) + if err != nil { + resp.Diagnostics.AddError("Error fetching Leaked Credential Check user-defined detection patterns", err.Error()) + return + } + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r *LeakedCredentialCheckRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LeakedCredentialCheckRulesModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + zoneID := cloudflare.ZoneIdentifier(state.ZoneID.ValueString()) + deleteParam := cloudflare.LeakedCredentialCheckDeleteDetectionParams{DetectionID: state.ID.ValueString()} + _, err := r.client.V1.LeakedCredentialCheckDeleteDetection(ctx, zoneID, deleteParam) + if err != nil { + resp.Diagnostics.AddError("Error deleting a user-defined detection patter for Leaked Credential Check", err.Error()) + return + } +} + +func (r *LeakedCredentialCheckRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) + return +} diff --git a/internal/framework/service/leaked_credential_check_rule/resource_test.go b/internal/framework/service/leaked_credential_check_rule/resource_test.go new file mode 100644 index 0000000000..88cce88b09 --- /dev/null +++ b/internal/framework/service/leaked_credential_check_rule/resource_test.go @@ -0,0 +1,102 @@ +package leaked_credential_check_rule_test + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + + cfv1 "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" + "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func init() { + resource.AddTestSweepers("cloudflare_leaked_credential_check_rule", &resource.Sweeper{ + Name: "cloudflare_leaked_credential_check_rule", + F: testSweepCloudflareLCCRules, + }) +} + +func testSweepCloudflareLCCRules(r string) error { + ctx := context.Background() + client, clientErr := acctest.SharedV1Client() + if clientErr != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr)) + } + + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + if zoneID == "" { + return errors.New("CLOUDFLARE_ZONE_ID must be set") + } + // fetch existing rules from API + rules, err := client.LeakedCredentialCheckListDetections(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.LeakedCredentialCheckListDetectionsParams{}) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Error fetching Leaked Credential Check user-defined detection patterns: %s", err)) + return err + } + for _, rule := range rules { + deleteParam := cfv1.LeakedCredentialCheckDeleteDetectionParams{DetectionID: rule.ID} + _, err := client.LeakedCredentialCheckDeleteDetection(ctx, cfv1.ZoneIdentifier(zoneID), deleteParam) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Error deleting a user-defined detection patter for Leaked Credential Check: %s", err)) + } + + } + return nil +} + +func TestAccCloudflareLeakedCredentialCheckRule_Basic(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_leaked_credential_check_rule.%s", rnd) + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.TestAccPreCheck(t) + }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCTwoSimpleRules(rnd)), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name+"_first", "zone_id", zoneID), + resource.TestCheckResourceAttr(name+"_first", "username", "lookup_json_string(http.request.body.raw, \"user\")"), + resource.TestCheckResourceAttr(name+"_first", "password", "lookup_json_string(http.request.body.raw, \"pass\")"), + + resource.TestCheckResourceAttr(name+"_second", "zone_id", zoneID), + resource.TestCheckResourceAttr(name+"_second", "username", "lookup_json_string(http.request.body.raw, \"id\")"), + resource.TestCheckResourceAttr(name+"_second", "password", "lookup_json_string(http.request.body.raw, \"secret\")"), + ), + }, + }, + }) +} + +func testAccConfigAddHeader(name, zoneID, config string) string { + header := fmt.Sprintf(` + resource "cloudflare_leaked_credential_check" "%[1]s" { + zone_id = "%[2]s" + enabled = true + }`, name, zoneID) + return header + "\n" + config +} + +func testAccLCCTwoSimpleRules(name string) string { + return fmt.Sprintf(` + resource "cloudflare_leaked_credential_check_rule" "%[1]s_first" { + zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id + username = "lookup_json_string(http.request.body.raw, \"user\")" + password = "lookup_json_string(http.request.body.raw, \"pass\")" + } + + resource "cloudflare_leaked_credential_check_rule" "%[1]s_second" { + zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id + username = "lookup_json_string(http.request.body.raw, \"id\")" + password = "lookup_json_string(http.request.body.raw, \"secret\")" + }`, name) +} diff --git a/internal/framework/service/leaked_credential_check_rule/schema.go b/internal/framework/service/leaked_credential_check_rule/schema.go new file mode 100644 index 0000000000..6f580e96a3 --- /dev/null +++ b/internal/framework/service/leaked_credential_check_rule/schema.go @@ -0,0 +1,33 @@ +package leaked_credential_check_rule + +import ( + "context" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func (r *LeakedCredentialCheckRuleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provides a Cloudflare Leaked Credential Check Rules resource for managing user-defined Leaked Credential detection patterns within a specific zone.", + Attributes: map[string]schema.Attribute{ + consts.IDSchemaKey: schema.StringAttribute{ + Description: consts.IDSchemaDescription, + Computed: true, + }, + consts.ZoneIDSchemaKey: schema.StringAttribute{ + Description: consts.ZoneIDSchemaDescription, + Required: true, + }, + "username": schema.StringAttribute{ + Description: "The ruleset expression to use in matching the username in a request.", + Required: true, + }, + "password": schema.StringAttribute{ + Description: "The ruleset expression to use in matching the password in a request", + Required: true, + }, + }, + } +} diff --git a/internal/framework/service/leaked_credential_check_rules/resource.go b/internal/framework/service/leaked_credential_check_rules/resource.go deleted file mode 100644 index ac34bc7362..0000000000 --- a/internal/framework/service/leaked_credential_check_rules/resource.go +++ /dev/null @@ -1,230 +0,0 @@ -package leaked_credential_check_rules - -import ( - "context" - "fmt" - - "github.com/cloudflare/cloudflare-go" - "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -var ( - _ resource.Resource = &LeakedCredentialCheckRulesResource{} - _ resource.ResourceWithImportState = &LeakedCredentialCheckRulesResource{} -) - -func NewResource() resource.Resource { - return &LeakedCredentialCheckRulesResource{} -} - -type LeakedCredentialCheckRulesResource struct { - client *muxclient.Client -} - -func (r *LeakedCredentialCheckRulesResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_leaked_credential_check_rules" -} - -func (r *LeakedCredentialCheckRulesResource) Configure(_ 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 *LeakedCredentialCheckRulesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan LeakedCredentialCheckRulesModel - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - zoneID := plan.ZoneID.ValueString() - var createdRules []LCCRuleValueModel - if len(plan.Rules) > 0 { - for _, rule := range plan.Rules { - createParam := cloudflare.LeakedCredentialCheckCreateDetectionParams{ - Username: rule.Username.ValueString(), - Password: rule.Password.ValueString(), - } - detection, err := r.client.V1.LeakedCredentialCheckCreateDetection(ctx, cloudflare.ZoneIdentifier(zoneID), createParam) - if err != nil { - resp.Diagnostics.AddError("Error creating a user-defined detection patter for Leaked Credential Check", err.Error()) - return - } - createdRules = append(createdRules, LCCRuleValueModel{ID: types.StringValue(detection.ID), Username: rule.Username, Password: rule.Password}) - } - - } - plan.Rules = createdRules - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) -} - -func (r *LeakedCredentialCheckRulesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state LeakedCredentialCheckRulesModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - zoneID := state.ZoneID.ValueString() - rules, err := r.client.V1.LeakedCredentialCheckListDetections(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.LeakedCredentialCheckListDetectionsParams{}) - if err != nil { - resp.Diagnostics.AddError("Error listing Leaked Credential Check user-defined detection patterns", err.Error()) - return - } - var readRules []LCCRuleValueModel - for _, rule := range rules { - readRules = append(readRules, buildLCCRuleValueModel(rule)) - } - state.Rules = readRules - diags = resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) -} - -func (r *LeakedCredentialCheckRulesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan LeakedCredentialCheckRulesModel - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - zoneID := cloudflare.ZoneIdentifier(plan.ZoneID.ValueString()) - // fetch existing rules from API - existing_rules, err := r.client.V1.LeakedCredentialCheckListDetections(ctx, zoneID, cloudflare.LeakedCredentialCheckListDetectionsParams{}) - if err != nil { - resp.Diagnostics.AddError("Error fetching Leaked Credential Check user-defined detection patterns", err.Error()) - return - } - // compare and create/delete accordingly - var createdRules []LCCRuleValueModel - toAdd, toRemove, toKeep := diffRules(plan.Rules, existing_rules) - // create - for _, createParam := range toAdd { - detection, err := r.client.V1.LeakedCredentialCheckCreateDetection(ctx, zoneID, createParam) - if err != nil { - resp.Diagnostics.AddError("Error creating a user-defined detection patter for Leaked Credential Check", err.Error()) - return - } - createdRules = append(createdRules, buildLCCRuleValueModel(detection)) - } - plan.Rules = createdRules // update plan rules with the newly created rules - // delete - for _, deleteParam := range toRemove { - _, err := r.client.V1.LeakedCredentialCheckDeleteDetection(ctx, zoneID, deleteParam) - if err != nil { - resp.Diagnostics.AddError("Error deleting a user-defined detection patter for Leaked Credential Check", err.Error()) - return - } - } - // add the existing rules we kept to the plan, if any - for _, kr := range toKeep { - plan.Rules = append(plan.Rules, buildLCCRuleValueModel(kr)) - } - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) -} - -// Delete all user-defined detection rules -func (r *LeakedCredentialCheckRulesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state LeakedCredentialCheckRulesModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - zoneID := cloudflare.ZoneIdentifier(state.ZoneID.ValueString()) - // fetch existing rules from API - rules, err := r.client.V1.LeakedCredentialCheckListDetections(ctx, zoneID, cloudflare.LeakedCredentialCheckListDetectionsParams{}) - if err != nil { - resp.Diagnostics.AddError("Error fetching Leaked Credential Check user-defined detection patterns", err.Error()) - return - } - for _, rule := range rules { - deleteParam := cloudflare.LeakedCredentialCheckDeleteDetectionParams{DetectionID: rule.ID} - _, err := r.client.V1.LeakedCredentialCheckDeleteDetection(ctx, zoneID, deleteParam) - if err != nil { - resp.Diagnostics.AddError("Error deleting a user-defined detection patter for Leaked Credential Check", err.Error()) - return - } - - } - -} - -func (r *LeakedCredentialCheckRulesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // req.ID is the zoneID for which you want to import the state of the - // Leaked Credential Check Rules - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone_id"), req.ID)...) - return -} - -func diffRules(desired []LCCRuleValueModel, current []cloudflare.LeakedCredentialCheckDetectionEntry) (toAdd []cloudflare.LeakedCredentialCheckCreateDetectionParams, toRemove []cloudflare.LeakedCredentialCheckDeleteDetectionParams, toKeep []cloudflare.LeakedCredentialCheckDetectionEntry) { - // Create a map for the desired rules - desiredMap := make(map[string]struct{}) - for _, d := range desired { - key := d.Username.ValueString() + ":" + d.Password.ValueString() // Use a unique key for comparison - desiredMap[key] = struct{}{} - } - - // Create a map for the current rules - currentMap := make(map[string]struct{}) - for _, c := range current { - key := c.Username + ":" + c.Password - currentMap[key] = struct{}{} - - // If a rule exists in current but not in desired, mark it for removal - if _, exists := desiredMap[key]; !exists { - toRemove = append(toRemove, cloudflare.LeakedCredentialCheckDeleteDetectionParams{DetectionID: c.ID}) - } else { - // If a rule exists both in current and desired, mark it as to keep - toKeep = append( - toKeep, - cloudflare.LeakedCredentialCheckDetectionEntry{ID: c.ID, Username: c.Username, Password: c.Password}, - ) - } - } - - // Find rules that exist in desired but not in current - for _, d := range desired { - key := d.Username.ValueString() + ":" + d.Password.ValueString() - if _, exists := currentMap[key]; !exists { - toAdd = append(toAdd, cloudflare.LeakedCredentialCheckCreateDetectionParams{ - Username: d.Username.ValueString(), - Password: d.Password.ValueString(), - }) - } - } - - return -} - -func buildLCCRuleValueModel(lccEntry cloudflare.LeakedCredentialCheckDetectionEntry) LCCRuleValueModel { - rule := LCCRuleValueModel{ - ID: types.StringValue(lccEntry.ID), - Username: types.StringValue(lccEntry.Username), - Password: types.StringValue(lccEntry.Password), - } - return rule -} diff --git a/internal/framework/service/leaked_credential_check_rules/resource_test.go b/internal/framework/service/leaked_credential_check_rules/resource_test.go deleted file mode 100644 index 781b04ce10..0000000000 --- a/internal/framework/service/leaked_credential_check_rules/resource_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package leaked_credential_check_rules_test - -import ( - "context" - "errors" - "fmt" - "os" - "regexp" - "testing" - - cfv1 "github.com/cloudflare/cloudflare-go" - "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" - "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func init() { - resource.AddTestSweepers("cloudflare_leaked_credential_check_rules", &resource.Sweeper{ - Name: "cloudflare_leaked_credential_check_rules", - F: testSweepCloudflareLCCRules, - }) -} - -func testSweepCloudflareLCCRules(r string) error { - ctx := context.Background() - client, clientErr := acctest.SharedV1Client() - if clientErr != nil { - tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr)) - } - - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - if zoneID == "" { - return errors.New("CLOUDFLARE_ZONE_ID must be set") - } - // fetch existing rules from API - rules, err := client.LeakedCredentialCheckListDetections(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.LeakedCredentialCheckListDetectionsParams{}) - if err != nil { - tflog.Error(ctx, fmt.Sprintf("Error fetching Leaked Credential Check user-defined detection patterns: %s", err)) - return err - } - for _, rule := range rules { - deleteParam := cfv1.LeakedCredentialCheckDeleteDetectionParams{DetectionID: rule.ID} - _, err := client.LeakedCredentialCheckDeleteDetection(ctx, cfv1.ZoneIdentifier(zoneID), deleteParam) - if err != nil { - tflog.Error(ctx, fmt.Sprintf("Error deleting a user-defined detection patter for Leaked Credential Check: %s", err)) - } - - } - return nil -} - -func TestAccCloudflareLeakedCredentialCheckRules_CRUD(t *testing.T) { - rnd := utils.GenerateRandomResourceName() - name := fmt.Sprintf("cloudflare_leaked_credential_check_rules.%s", rnd) - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - - var detectionRules []cfv1.LeakedCredentialCheckDetectionEntry - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - acctest.TestAccPreCheck(t) - }, - ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCTwoSimpleRules(rnd)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - resource.TestCheckResourceAttr(name, "rule.#", "2"), - resource.TestCheckResourceAttr(name, "rule.0.%", "3"), - resource.TestCheckResourceAttr(name, "rule.1.%", "3"), - resource.TestMatchTypeSetElemNestedAttrs( - name, - "rule.*", - map[string]*regexp.Regexp{ - "id": regexp.MustCompile(`^[a-z0-9]+$`), - "username": regexp.MustCompile(`"id"`), - "password": regexp.MustCompile(`"secret"`), - }, - ), - resource.TestMatchTypeSetElemNestedAttrs( - name, - "rule.*", - map[string]*regexp.Regexp{ - "id": regexp.MustCompile(`^[a-z0-9]+$`), - "username": regexp.MustCompile(`"user"`), - "password": regexp.MustCompile(`"pass"`), - }, - ), - testAccCheckLCCNumRules(name, 2, &detectionRules), - ), - }, - { // remove one rule, keep one, and add a new one - Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCSimpleChangeOneRule(rnd)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - resource.TestCheckResourceAttr(name, "rule.#", "2"), - resource.TestCheckResourceAttr(name, "rule.0.%", "3"), - resource.TestCheckResourceAttr(name, "rule.1.%", "3"), - resource.TestMatchTypeSetElemNestedAttrs( - name, - "rule.*", - map[string]*regexp.Regexp{ - "id": regexp.MustCompile(`^[a-z0-9]+$`), - "username": regexp.MustCompile(`"name"`), - "password": regexp.MustCompile(`"key"`), - }, - ), - testAccCheckLCCOneRuleChange(name, 2, &detectionRules), - ), - }, - { // clear rules - Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCNoRules(rnd)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - resource.TestCheckResourceAttr(name, "rule.#", "0"), - testAccCheckLCCNumRules(name, 0, &detectionRules), - ), - }, - { // add a single rule - Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCOneSimpleRule(rnd)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - resource.TestCheckResourceAttr(name, "rule.#", "1"), - resource.TestCheckResourceAttr(name, "rule.0.%", "3"), - resource.TestCheckResourceAttr(name, "rule.0.username", "lookup_json_string(http.request.body.raw, \"username\")"), - resource.TestCheckResourceAttr(name, "rule.0.password", "lookup_json_string(http.request.body.raw, \"password\")"), - testAccCheckLCCNumRules(name, 1, &detectionRules), - ), - }, - }, - }) -} - -func testAccConfigAddHeader(name, zoneID, config string) string { - header := fmt.Sprintf(` - resource "cloudflare_leaked_credential_check" "%[1]s" { - zone_id = "%[2]s" - enabled = true - }`, name, zoneID) - return header + "\n" + config -} - -func testAccLCCTwoSimpleRules(name string) string { - return fmt.Sprintf(` - resource "cloudflare_leaked_credential_check_rules" "%[1]s" { - zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id - rule { - username = "lookup_json_string(http.request.body.raw, \"user\")" - password = "lookup_json_string(http.request.body.raw, \"pass\")" - } - rule { - username = "lookup_json_string(http.request.body.raw, \"id\")" - password = "lookup_json_string(http.request.body.raw, \"secret\")" - } - }`, name) -} - -func testAccLCCSimpleChangeOneRule(name string) string { - return fmt.Sprintf(` - resource "cloudflare_leaked_credential_check_rules" "%[1]s" { - zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id - rule { - username = "lookup_json_string(http.request.body.raw, \"name\")" - password = "lookup_json_string(http.request.body.raw, \"key\")" - } - rule { - username = "lookup_json_string(http.request.body.raw, \"id\")" - password = "lookup_json_string(http.request.body.raw, \"secret\")" - } - }`, name) -} - -func testAccLCCNoRules(name string) string { - return fmt.Sprintf(` - resource "cloudflare_leaked_credential_check_rules" "%[1]s" { - zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id - }`, name) -} - -func testAccLCCOneSimpleRule(name string) string { - return fmt.Sprintf(` - resource "cloudflare_leaked_credential_check_rules" "%[1]s" { - zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id - rule { - username = "lookup_json_string(http.request.body.raw, \"username\")" - password = "lookup_json_string(http.request.body.raw, \"password\")" - } - }`, name) -} - -func testAccCheckLCCNumRules(name string, length int, rules *[]cfv1.LeakedCredentialCheckDetectionEntry) resource.TestCheckFunc { - return func(s *terraform.State) error { - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - _, ok := s.RootModule().Resources[name] - if !ok { - return fmt.Errorf("not found: %s", name) - } - client, _ := acctest.SharedV1Client() - detections, err := client.LeakedCredentialCheckListDetections( - context.Background(), - cfv1.ZoneIdentifier(zoneID), - cfv1.LeakedCredentialCheckListDetectionsParams{}, - ) - if err != nil { - return err - } - - if length != len(detections) { - return fmt.Errorf("Expected num of rules (%d) does not match the actual num (%d)", length, len(detections)) - } - *rules = detections - - return nil - } -} - -func testAccCheckLCCOneRuleChange(name string, length int, rules *[]cfv1.LeakedCredentialCheckDetectionEntry) resource.TestCheckFunc { - return func(s *terraform.State) error { - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - _, ok := s.RootModule().Resources[name] - if !ok { - return fmt.Errorf("not found: %s", name) - } - client, _ := acctest.SharedV1Client() - detections, err := client.LeakedCredentialCheckListDetections( - context.Background(), - cfv1.ZoneIdentifier(zoneID), - cfv1.LeakedCredentialCheckListDetectionsParams{}, - ) - if err != nil { - return err - } - - if length != len(detections) { - return fmt.Errorf("Expected num of rules (%d) does not match the actual num (%d)", length, len(detections)) - } - previousRulesMap := make(map[string]string) - for _, prul := range *rules { - mapkey := prul.Password + "|" + prul.Username - previousRulesMap[mapkey] = prul.ID - } - - found := false - for _, det := range detections { - mapkey := det.Password + "|" + det.Username - if _, exists := previousRulesMap[mapkey]; exists { - if previousRulesMap[mapkey] != det.ID { - return errors.New("Found the unchanged rule but the ID is different") - } - found = true - } - } - if !found { - return fmt.Errorf("Could not find the rule that was not changed!") - } - return nil - } -} diff --git a/internal/framework/service/leaked_credential_check_rules/schema.go b/internal/framework/service/leaked_credential_check_rules/schema.go deleted file mode 100644 index 41e19ddba9..0000000000 --- a/internal/framework/service/leaked_credential_check_rules/schema.go +++ /dev/null @@ -1,42 +0,0 @@ -package leaked_credential_check_rules - -import ( - "context" - - "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" -) - -func (r *LeakedCredentialCheckRulesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Provides a Cloudflare Leaked Credential Check Rules resource for managing user-defined Leaked Credential detection patterns within a specific zone.", - Attributes: map[string]schema.Attribute{ - consts.ZoneIDSchemaKey: schema.StringAttribute{ - Description: consts.ZoneIDSchemaDescription, - Required: true, - }, - }, - Blocks: map[string]schema.Block{ - "rule": schema.SetNestedBlock{ - Description: "List of user-defined patterns for Leaked Credential Check", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - consts.IDSchemaKey: schema.StringAttribute{ - Description: consts.IDSchemaDescription, - Computed: true, - }, - "username": schema.StringAttribute{ - Description: "The ruleset expression to use in matching the username in a request.", - Required: true, - }, - "password": schema.StringAttribute{ - Description: "The ruleset expression to use in matching the password in a request", - Required: true, - }, - }, - }, - }, - }, - } -}