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 26, 2022
1 parent d34b626 commit 96a6b00
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 1 deletion.
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:enhancement
resource/cloudflare_dlp_profile: adds support for DLP profiles
```
2 changes: 1 addition & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.19.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down
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
217 changes: 217 additions & 0 deletions internal/provider/resource_cloudflare_dlp_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package provider

import (
"context"
"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())
if err != nil {
tflog.Info(ctx, fmt.Sprintf("DLP Profile %s no longer exists", d.Id()))
d.SetId("")
return nil
}

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.SplitN(d.Id(), "/", 3)
if len(attributes) != 3 {
return nil, fmt.Errorf(
"invalid id (%q) specified, should be in format %q",
d.Id(),
"account/accountID/dlpProfileID",
)
}
identifierType, identifierID, dlpProfileID := attributes[0], attributes[1], attributes[2]

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

//lintignore:R001
d.Set(fmt.Sprintf("%s_id", identifierType), identifierID)
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 96a6b00

Please sign in to comment.