Skip to content

Commit

Permalink
DLP-655 Adds support for DLP profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
Eduardo Gomes committed Oct 28, 2022
1 parent 72ed249 commit a7dbcf6
Show file tree
Hide file tree
Showing 7 changed files with 476 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/1984.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_dlp_profile
```
1 change: 1 addition & 0 deletions examples/resources/cloudflare_dlp_profile/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_dlp_profile.example <account_id>/<dlp_profile_id>
42 changes: 42 additions & 0 deletions examples/resources/cloudflare_dlp_profile/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Predefined profile
resource "cloudflare_dlp_profile" "example_predefined" {
account_id = "0da42c8d2132a9ddaf714f9e7c920711"
name = "Example Predefined Profile"
type = "predefined"

entry {
name = "Mastercard Card Number"
enabled = true
}

entry {
name = "Union Pay Card Number"
enabled = false
}
}

# Custom profile
resource "cloudflare_dlp_profile" "example_custom" {
account_id = "0da42c8d2132a9ddaf714f9e7c920711"
name = "Example Custom Profile"
description = "A profile with example entries"
type = "custom"

entry {
name = "Matches visa credit cards"
enabled = true
pattern {
regex = "4\d{3}([-\. ])?\d{4}([-\. ])?\d{4}([-\. ])?\d{4}"
validation = "luhn"
}
}

entry {
name = "Matches diners club card"
enabled = true
pattern {
regex = "(?:0[0-5]|[68][0-9])[0-9]{11}"
validation = "luhn"
}
}
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_device_policy_certificates": resourceCloudflareDevicePolicyCertificates(),
"cloudflare_device_posture_integration": resourceCloudflareDevicePostureIntegration(),
"cloudflare_device_posture_rule": resourceCloudflareDevicePostureRule(),
"cloudflare_dlp_profile": resourceCloudflareDLPProfile(),
"cloudflare_email_routing_address": resourceCloudflareEmailRoutingAddress(),
"cloudflare_email_routing_catch_all": resourceCloudflareEmailRoutingCatchAll(),
"cloudflare_email_routing_rule": resourceCloudflareEmailRoutingRule(),
Expand Down
222 changes: 222 additions & 0 deletions internal/provider/resource_cloudflare_dlp_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package provider

import (
"context"
"errors"
"fmt"
"github.com/cloudflare/cloudflare-go"
"strings"

"github.com/MakeNowJust/heredoc/v2"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceCloudflareDLPProfile() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareDLPProfileSchema(),
CreateContext: resourceCloudflareDLPProfileCreate,
ReadContext: resourceCloudflareDLPProfileRead,
UpdateContext: resourceCloudflareDLPProfileUpdate,
DeleteContext: resourceCloudflareDLPProfileDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareDLPProfileImport,
},
Description: heredoc.Doc(`
Provides a Cloudflare DLP Profile resource. Data Loss Prevention profiles
are a set of entries that can be matched in HTTP bodies or files.
They are referenced in Zero Trust Gateway rules.
`),
}
}

func dlpPatternToSchema(pattern cloudflare.DLPPattern) map[string]interface{} {
schema := make(map[string]interface{})
if pattern.Regex != "" {
schema["regex"] = pattern.Regex
}
if pattern.Validation != "" {
schema["validation"] = pattern.Validation
}
return schema
}

func dlpPatternToAPI(pattern map[string]interface{}) cloudflare.DLPPattern {
entryPattern := cloudflare.DLPPattern{
Regex: pattern["regex"].(string),
}
if validation, ok := pattern["validation"].(string); ok {
entryPattern.Validation = validation
}
return entryPattern
}

func dlpEntryToSchema(entry cloudflare.DLPEntry) map[string]interface{} {
entrySchema := make(map[string]interface{})
if entry.ID != "" {
entrySchema["id"] = entry.ID
}
if entry.Name != "" {
entrySchema["name"] = entry.Name
}
entrySchema["enabled"] = entry.Enabled != nil && *entry.Enabled == true
if entry.Pattern != nil {
entrySchema["pattern"] = []interface{}{dlpPatternToSchema(*entry.Pattern)}
}
return entrySchema
}

func dlpEntryToAPI(entryType string, entryMap map[string]interface{}) cloudflare.DLPEntry {
apiEntry := cloudflare.DLPEntry{
Name: entryMap["name"].(string),
}
if entryID, ok := entryMap["id"].(string); ok {
apiEntry.ID = entryID
}
if patterns, ok := entryMap["pattern"].([]interface{}); ok && len(patterns) != 0 {
newPattern := dlpPatternToAPI(patterns[0].(map[string]interface{}))
apiEntry.Pattern = &newPattern
}
enabled := entryMap["enabled"] == true
apiEntry.Enabled = &enabled
apiEntry.Type = entryType
return apiEntry
}

func resourceCloudflareDLPProfileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

identifier := cloudflare.AccountIdentifier(d.Get("account_id").(string))
dlpProfile, err := client.GetDLPProfile(ctx, identifier, d.Id())
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("DLP Profile %s no longer exists", d.Id()))
d.SetId("")
return nil
}
if err != nil {
return diag.FromErr(fmt.Errorf("error reading DLP profile: %w", err))
}

d.Set("name", dlpProfile.Name)
d.Set("type", dlpProfile.Type)
if dlpProfile.Description != "" {
d.Set("description", dlpProfile.Description)
}
entries := make([]interface{}, 0, len(dlpProfile.Entries))
for _, entry := range dlpProfile.Entries {
entries = append(entries, dlpEntryToSchema(entry))
}
d.Set("entry", schema.NewSet(schema.HashResource(&schema.Resource{
Schema: resourceCloudflareDLPEntrySchema(),
}), entries))

return nil
}

func resourceCloudflareDLPProfileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
identifier := cloudflare.AccountIdentifier(d.Get("account_id").(string))

newDLPProfile := cloudflare.DLPProfile{
Name: d.Get("name").(string),
Type: d.Get("type").(string),
Description: d.Get("description").(string),
}

if newDLPProfile.Type == DLPProfileTypePredefined {
return diag.FromErr(fmt.Errorf("predefined DLP profiles cannot be created and must be imported"))
}

if entries, ok := d.GetOk("entry"); ok {
for _, entry := range entries.(*schema.Set).List() {
newDLPProfile.Entries = append(newDLPProfile.Entries, dlpEntryToAPI(newDLPProfile.Type, entry.(map[string]interface{})))
}
}

dlpProfiles, err := client.CreateDLPProfiles(ctx, identifier, cloudflare.CreateDLPProfilesParams{
Profiles: []cloudflare.DLPProfile{newDLPProfile},
Type: newDLPProfile.Type,
})
if err != nil {
return diag.FromErr(fmt.Errorf("error creating DLP Profile for name %s: %w", newDLPProfile.Name, err))
}
if len(dlpProfiles) == 0 {
return diag.FromErr(fmt.Errorf("error creating DLP Profile for name %s: no profile in response", newDLPProfile.Name))
}

d.SetId(dlpProfiles[0].ID)
return resourceCloudflareDLPProfileRead(ctx, d, meta)
}

func resourceCloudflareDLPProfileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

updatedDLPProfile := cloudflare.DLPProfile{
ID: d.Id(),
Name: d.Get("name").(string),
Type: d.Get("type").(string),
}
updatedDLPProfile.Description, _ = d.Get("description").(string)
if entries, ok := d.GetOk("entry"); ok {
for _, entry := range entries.(*schema.Set).List() {
updatedDLPProfile.Entries = append(updatedDLPProfile.Entries, dlpEntryToAPI(updatedDLPProfile.Type, entry.(map[string]interface{})))
}
}

tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare DLP Profile from struct: %+v", updatedDLPProfile))

identifier := cloudflare.AccountIdentifier(d.Get("account_id").(string))
dlpProfile, err := client.UpdateDLPProfile(ctx, identifier, cloudflare.UpdateDLPProfileParams{
ProfileID: updatedDLPProfile.ID,
Profile: updatedDLPProfile,
Type: updatedDLPProfile.Type,
})
if err != nil {
return diag.FromErr(fmt.Errorf("error updating DLP profile for ID %q: %w", d.Id(), err))
}
if dlpProfile.ID == "" {
return diag.FromErr(fmt.Errorf("failed to find DLP Profile ID in update response; resource was empty"))
}

return resourceCloudflareDLPProfileRead(ctx, d, meta)
}

func resourceCloudflareDLPProfileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare DLP Profile using ID: %s", d.Id()))

profileType, _ := d.Get("type").(string)
if profileType != DLPProfileTypeCustom {
return diag.FromErr(fmt.Errorf("error deleting DLP Profile: can only delete custom profiles"))
}
identifier := cloudflare.AccountIdentifier(d.Get("account_id").(string))
if err := client.DeleteDLPProfile(ctx, identifier, d.Id()); err != nil {
return diag.FromErr(fmt.Errorf("error deleting DLP Profile for ID %q: %w", d.Id(), err))
}

resourceCloudflareDLPProfileRead(ctx, d, meta)
return nil
}

func resourceCloudflareDLPProfileImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.Split(d.Id(), "/")
if len(attributes) != 2 {
return nil, fmt.Errorf(
"invalid id (%q) specified, should be in format %q",
d.Id(),
"accountID/dlpProfileID",
)
}
accountID, dlpProfileID := attributes[0], attributes[1]

tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare DLP Profile: %q, ID %q", accountID, dlpProfileID))

//lintignore:R001
d.Set("account_id", accountID)
d.SetId(dlpProfileID)

resourceCloudflareDLPProfileRead(ctx, d, meta)
return []*schema.ResourceData{d}, nil
}
113 changes: 113 additions & 0 deletions internal/provider/resource_cloudflare_dlp_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package provider

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccCloudflareDLPProfile_Custom(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_dlp_profile.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheckAccount(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCloudflareDLPProfileConfigCustom(accountID, rnd, "custom profile"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "account_id", accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "description", "custom profile"),
resource.TestCheckResourceAttr(name, "type", "custom"),
resource.TestCheckResourceAttr(name, "entry.0.name", fmt.Sprintf("%s_entry1", rnd)),
resource.TestCheckResourceAttr(name, "entry.0.enabled", "true"),
resource.TestCheckResourceAttr(name, "entry.0.pattern.0.regex", "^4[0-9]"),
resource.TestCheckResourceAttr(name, "entry.0.pattern.0.validation", "luhn"),
),
},
},
})
}

func TestAccCloudflareDLPProfile_Custom_MultipleEntries(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_dlp_profile.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheckAccount(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCloudflareDLPProfileConfigCustomMultipleEntries(accountID, rnd, "custom profile 2"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "account_id", accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "description", "custom profile 2"),
resource.TestCheckResourceAttr(name, "type", "custom"),
resource.TestCheckResourceAttr(name, "entry.0.name", fmt.Sprintf("%s_entry1", rnd)),
resource.TestCheckResourceAttr(name, "entry.0.enabled", "true"),
resource.TestCheckResourceAttr(name, "entry.0.pattern.0.regex", "^4[0-9]"),
resource.TestCheckResourceAttr(name, "entry.0.pattern.0.validation", "luhn"),
resource.TestCheckResourceAttr(name, "entry.1.name", fmt.Sprintf("%s_entry2", rnd)),
resource.TestCheckResourceAttr(name, "entry.1.enabled", "true"),
resource.TestCheckResourceAttr(name, "entry.1.pattern.0.regex", "^3[0-9]"),
resource.TestCheckResourceAttr(name, "entry.1.pattern.0.validation", "luhn"),
),
},
},
})
}

func testAccCloudflareDLPProfileConfigCustom(accountID, rnd, description string) string {
return fmt.Sprintf(`
resource "cloudflare_dlp_profile" "%[1]s" {
account_id = "%[3]s"
name = "%[1]s"
description = "%[2]s"
type = "custom"
entry {
name = "%[1]s_entry1"
enabled = true
pattern {
regex = "^4[0-9]"
validation = "luhn"
}
}
}
`, rnd, description, accountID)
}

func testAccCloudflareDLPProfileConfigCustomMultipleEntries(accountID, rnd, description string) string {
return fmt.Sprintf(`
resource "cloudflare_dlp_profile" "%[1]s" {
account_id = "%[3]s"
name = "%[1]s"
description = "%[2]s"
type = "custom"
entry {
name = "%[1]s_entry1"
enabled = true
pattern {
regex = "^4[0-9]"
validation = "luhn"
}
}
entry {
name = "%[1]s_entry2"
enabled = true
pattern {
regex = "^3[0-9]"
validation = "luhn"
}
}
}
`, rnd, description, accountID)
}

0 comments on commit a7dbcf6

Please sign in to comment.