Skip to content

Commit

Permalink
Merge pull request #4676 from mrusso19/add-new-lcc-rules-resource
Browse files Browse the repository at this point in the history
Add Leaked Credential Check Rules resource
  • Loading branch information
jacobbednarz authored Dec 5, 2024
2 parents 99c1406 + 078b57d commit 4fbd47a
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/4676.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_leaked_credential_check_rule
```
47 changes: 47 additions & 0 deletions docs/resources/leaked_credential_check_rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "cloudflare_leaked_credential_check_rule Resource - Cloudflare"
subcategory: ""
description: |-
Provides a Cloudflare Leaked Credential Check Rule resource for managing user-defined Leaked Credential detection patterns within a specific zone.
---

# cloudflare_leaked_credential_check_rule (Resource)

Provides a Cloudflare Leaked Credential Check Rule resource for managing user-defined Leaked Credential detection patterns within a specific zone.

## Example Usage

```terraform
# Enable the Leaked Credentials Check detection before trying
# to add detections.
resource "cloudflare_leaked_credential_check" "example" {
zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b"
enabled = true
}
resource "cloudflare_leaked_credential_check_rule" "example" {
zone_id = cloudflare_leaked_credential_check.example.zone_id
username = "lookup_json_string(http.request.body.raw, \"user\")"
password = "lookup_json_string(http.request.body.raw, \"pass\")"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `password` (String) The ruleset expression to use in matching the password in a request
- `username` (String) The ruleset expression to use in matching the username in a request.
- `zone_id` (String) The zone identifier to target for the resource.

### Read-Only

- `id` (String) The identifier of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import cloudflare_leaked_credential_check_rule.example <zone_id>/<resource_id>
```
2 changes: 1 addition & 1 deletion docs/resources/notification_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Read-Only:
Optional:

- `actions` (Set of String) Targeted actions for alert.
- `affected_components` (Set of String) Affected components for alert. Available values: `API`, `API Shield`, `Access`, `Always Online`, `Analytics`, `Apps Marketplace`, `Argo Smart Routing`, `Audit Logs`, `Authoritative DNS`, `Billing`, `Bot Management`, `Bring Your Own IP (BYOIP)`, `Browser Isolation`, `CDN Cache Purge`, `CDN/Cache`, `Cache Reserve`, `Challenge Platform`, `Cloud Access Security Broker (CASB)`, `Community Site`, `DNS Root Servers`, `DNS Updates`, `Dashboard`, `Data Loss Prevention (DLP)`, `Developer's Site`, `Digital Experience Monitoring (DEX)`, `Distributed Web Gateway`, `Durable Objects`, `Email Routing`, `Ethereum Gateway`, `Firewall`, `Gateway`, `Geo-Key Manager`, `Image Resizing`, `Images`, `Infrastructure`, `Lists`, `Load Balancing and Monitoring`, `Logs`, `Magic Firewall`, `Magic Transit`, `Magic WAN`, `Magic WAN Connector`, `Marketing Site`, `Mirage`, `Network`, `Notifications`, `Observatory`, `Page Shield`, `Pages`, `R2`, `Radar`, `Randomness Beacon`, `Recursive DNS`, `Registrar`, `Registration Data Access Protocol (RDAP)`, `SSL Certificate Provisioning`, `SSL for SaaS Provisioning`, `Security Center`, `Snippets`, `Spectrum`, `Speed Optimizations`, `Stream`, `Support Site`, `Time Services`, `Trace`, `Tunnel`, `Turnstile`, `WARP`, `Waiting Room`, `Web Analytics`, `Workers`, `Workers KV`, `Workers Preview`, `Zaraz`, `Zero Trust`, `Zero Trust Dashboard`, `Zone Versioning`.
- `affected_components` (Set of String) Affected components for alert. Available values: `API`, `API Shield`, `Access`, `Always Online`, `Analytics`, `Apps Marketplace`, `Argo Smart Routing`, `Audit Logs`, `Authoritative DNS`, `Billing`, `Bot Management`, `Bring Your Own IP (BYOIP)`, `Browser Isolation`, `CDN Cache Purge`, `CDN/Cache`, `Cache Reserve`, `Challenge Platform`, `Cloud Access Security Broker (CASB)`, `Community Site`, `D1`, `DNS Root Servers`, `DNS Updates`, `Dashboard`, `Data Loss Prevention (DLP)`, `Developer's Site`, `Digital Experience Monitoring (DEX)`, `Distributed Web Gateway`, `Durable Objects`, `Email Routing`, `Ethereum Gateway`, `Firewall`, `Gateway`, `Geo-Key Manager`, `Image Resizing`, `Images`, `Infrastructure`, `Lists`, `Load Balancing and Monitoring`, `Logs`, `Magic Firewall`, `Magic Transit`, `Magic WAN`, `Magic WAN Connector`, `Marketing Site`, `Mirage`, `Network`, `Notifications`, `Observatory`, `Page Shield`, `Pages`, `R2`, `Radar`, `Randomness Beacon`, `Recursive DNS`, `Registrar`, `Registration Data Access Protocol (RDAP)`, `SSL Certificate Provisioning`, `SSL for SaaS Provisioning`, `Security Center`, `Snippets`, `Spectrum`, `Speed Optimizations`, `Stream`, `Support Site`, `Time Services`, `Trace`, `Tunnel`, `Turnstile`, `WARP`, `Waiting Room`, `Web Analytics`, `Workers`, `Workers KV`, `Workers Preview`, `Zaraz`, `Zero Trust`, `Zero Trust Dashboard`, `Zone Versioning`.
- `airport_code` (Set of String) Filter on Points of Presence.
- `alert_trigger_preferences` (Set of String) Alert trigger preferences. Example: `slo`.
- `enabled` (Set of String) State of the pool to alert on.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import cloudflare_leaked_credential_check_rule.example <zone_id>/<resource_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Enable the Leaked Credentials Check detection before trying
# to add detections.
resource "cloudflare_leaked_credential_check" "example" {
zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b"
enabled = true
}

resource "cloudflare_leaked_credential_check_rule" "example" {
zone_id = cloudflare_leaked_credential_check.example.zone_id
username = "lookup_json_string(http.request.body.raw, \"user\")"
password = "lookup_json_string(http.request.body.raw, \"pass\")"
}
2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
"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"
Expand Down Expand Up @@ -393,6 +394,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_rule.NewResource,
}
}

Expand Down
10 changes: 10 additions & 0 deletions internal/framework/service/leaked_credential_check_rule/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package leaked_credential_check_rule

import "github.com/hashicorp/terraform-plugin-framework/types"

type LeakedCredentialCheckRulesModel struct {
ZoneID types.String `tfsdk:"zone_id"`
ID types.String `tfsdk:"id"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
}
161 changes: 161 additions & 0 deletions internal/framework/service/leaked_credential_check_rule/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package leaked_credential_check_rule

import (
"context"
"fmt"
"strings"

"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) {
idparts := strings.Split(req.ID, "/")
if len(idparts) != 2 {
resp.Diagnostics.AddError("error importing leaked credential detection", "invalid ID specified. Please specify the ID as \"zone_id/resource_id\"")
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("zone_id"), idparts[0],
)...)
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("id"), idparts[1],
)...)
}
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 4fbd47a

Please sign in to comment.