diff --git a/.changelog/4674.txt b/.changelog/4674.txt new file mode 100644 index 00000000000..ee94251181d --- /dev/null +++ b/.changelog/4674.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_leaked_credential_check +``` \ No newline at end of file diff --git a/docs/resources/leaked_credential_check.md b/docs/resources/leaked_credential_check.md new file mode 100644 index 00000000000..e96149539ef --- /dev/null +++ b/docs/resources/leaked_credential_check.md @@ -0,0 +1,35 @@ +--- +page_title: "cloudflare_leaked_credential_check Resource - Cloudflare" +subcategory: "" +description: |- + Provides a Cloudflare Leaked Credential Check resource to be used for managing the status of the Cloudflare Leaked Credential detection within a specific zone. +--- + +# cloudflare_leaked_credential_check (Resource) + +Provides a Cloudflare Leaked Credential Check resource to be used for managing the status of the Cloudflare Leaked Credential detection within a specific zone. + +## Example Usage + +```terraform +# Enable the Leaked Credentials Check detection +resource "cloudflare_leaked_credential_check" "example" { + zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b" + enabled = true +} +``` + +## Schema + +### Required + +- `enabled` (Boolean) State of the Leaked Credential Check detection +- `zone_id` (String) The zone identifier to target for the resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import cloudflare_leaked_credential_check.example +``` diff --git a/examples/resources/cloudflare_leaked_credential_check/import.sh b/examples/resources/cloudflare_leaked_credential_check/import.sh new file mode 100644 index 00000000000..aa643e4cc9e --- /dev/null +++ b/examples/resources/cloudflare_leaked_credential_check/import.sh @@ -0,0 +1 @@ +terraform import cloudflare_leaked_credential_check.example \ No newline at end of file diff --git a/examples/resources/cloudflare_leaked_credential_check/resource.tf b/examples/resources/cloudflare_leaked_credential_check/resource.tf new file mode 100644 index 00000000000..bd796d49562 --- /dev/null +++ b/examples/resources/cloudflare_leaked_credential_check/resource.tf @@ -0,0 +1,5 @@ +# Enable the Leaked Credentials Check detection +resource "cloudflare_leaked_credential_check" "example" { + zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b" + enabled = true +} \ No newline at end of file diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 2871f46e251..96508d0f628 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -28,6 +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_rules" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check" "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" @@ -389,6 +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_rules.NewResource, + leaked_credential_check.NewResource, } } diff --git a/internal/framework/service/leaked_credential_check/model.go b/internal/framework/service/leaked_credential_check/model.go new file mode 100644 index 00000000000..131d621c0d3 --- /dev/null +++ b/internal/framework/service/leaked_credential_check/model.go @@ -0,0 +1,8 @@ +package leaked_credential_check + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type LeakedCredentialCheckModel struct { + ZoneID types.String `tfsdk:"zone_id"` + Enabled types.Bool `tfsdk:"enabled"` +} diff --git a/internal/framework/service/leaked_credential_check/resource.go b/internal/framework/service/leaked_credential_check/resource.go new file mode 100644 index 00000000000..2686f7a7802 --- /dev/null +++ b/internal/framework/service/leaked_credential_check/resource.go @@ -0,0 +1,141 @@ +package leaked_credential_check + +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 = &LeakedCredentialCheckResource{} +) + +func NewResource() resource.Resource { + return &LeakedCredentialCheckResource{} +} + +type LeakedCredentialCheckResource struct { + client *muxclient.Client +} + +func (r *LeakedCredentialCheckResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_leaked_credential_check" +} + +func (r *LeakedCredentialCheckResource) 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 *LeakedCredentialCheckResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LeakedCredentialCheckModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := plan.ZoneID.ValueString() + createParams := cloudflare.LeakCredentialCheckSetStatusParams{ + Enabled: plan.Enabled.ValueBoolPointer(), + } + + _, err := r.client.V1.LeakedCredentialCheckSetStatus(ctx, cloudflare.ZoneIdentifier(zoneID), createParams) + if err != nil { + resp.Diagnostics.AddError("Error enabling (Create) Leaked Credential Check", err.Error()) + return + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +func (r *LeakedCredentialCheckResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LeakedCredentialCheckModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := state.ZoneID.ValueString() + status, err := r.client.V1.LeakedCredentialCheckGetStatus(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.LeakedCredentialCheckGetStatusParams{}) + if err != nil { + resp.Diagnostics.AddError("Error reading Leaked Credential Check status", err.Error()) + return + } + state.Enabled = types.BoolPointerValue(status.Enabled) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +func (r *LeakedCredentialCheckResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LeakedCredentialCheckModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := plan.ZoneID.ValueString() + updateParams := cloudflare.LeakCredentialCheckSetStatusParams{ + Enabled: plan.Enabled.ValueBoolPointer(), + } + + _, err := r.client.V1.LeakedCredentialCheckSetStatus(ctx, cloudflare.ZoneIdentifier(zoneID), updateParams) + if err != nil { + resp.Diagnostics.AddError("Error updating status of Leaked Credential Check", err.Error()) + return + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +// Delete disables (enabled: false) the Leaked Credential Check functionality +func (r *LeakedCredentialCheckResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LeakedCredentialCheckModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := state.ZoneID.ValueString() + params := cloudflare.LeakCredentialCheckSetStatusParams{ + Enabled: cloudflare.BoolPtr(false), + } + + _, err := r.client.V1.LeakedCredentialCheckSetStatus(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + resp.Diagnostics.AddError("Error updating status of Leaked Credential Check", err.Error()) + return + } +} + +func (r *LeakedCredentialCheckResource) 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 feature + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone_id"), req.ID)...) +} diff --git a/internal/framework/service/leaked_credential_check/resource_test.go b/internal/framework/service/leaked_credential_check/resource_test.go new file mode 100644 index 00000000000..ec642cc8622 --- /dev/null +++ b/internal/framework/service/leaked_credential_check/resource_test.go @@ -0,0 +1,89 @@ +package leaked_credential_check_test + +import ( + "context" + "errors" + "fmt" + "log" + "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", &resource.Sweeper{ + Name: "cloudflare_leaked_credential_check", + F: testSweepCloudflareLCC, + }) +} + +func testSweepCloudflareLCC(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") + } + + status, err := client.LeakedCredentialCheckGetStatus(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.LeakedCredentialCheckGetStatusParams{}) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to GET Leaked Credential status: %s", err)) + } + + if *status.Enabled == false { + log.Print("[DEBUG] LCC already disabled") + return nil + } + _, err = client.LeakedCredentialCheckSetStatus(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.LeakCredentialCheckSetStatusParams{Enabled: cfv1.BoolPtr(false)}) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to disable Leaked Credential Check: %s", err)) + return err + } + + return nil +} + +func TestAccCloudflareLeakedCredentialCheck_Basic(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_leaked_credential_check.%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: testAccLCCSimple(rnd, zoneID, "true"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "enabled", "true"), + ), + }, + { + Config: testAccLCCSimple(rnd, zoneID, "false"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "enabled", "false"), + ), + }, + }, + }) +} + +func testAccLCCSimple(ID, zoneID, enabled string) string { + return fmt.Sprintf(` + resource "cloudflare_leaked_credential_check" "%[1]s" { + zone_id = "%[2]s" + enabled = "%[3]s" + }`, ID, zoneID, enabled) +} diff --git a/internal/framework/service/leaked_credential_check/schema.go b/internal/framework/service/leaked_credential_check/schema.go new file mode 100644 index 00000000000..aed205321ad --- /dev/null +++ b/internal/framework/service/leaked_credential_check/schema.go @@ -0,0 +1,25 @@ +package leaked_credential_check + +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 *LeakedCredentialCheckResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provides a Cloudflare Leaked Credential Check resource to be used for managing the status of the Cloudflare Leaked Credential detection within a specific zone.", + Attributes: map[string]schema.Attribute{ + consts.ZoneIDSchemaKey: schema.StringAttribute{ + Description: consts.ZoneIDSchemaDescription, + Required: true, + }, + "enabled": schema.BoolAttribute{ + Description: "State of the Leaked Credential Check detection", + Required: true, + }, + }, + } +}