Skip to content

Commit

Permalink
Merge pull request #1984 from GreenStage/egomes/DLP-655
Browse files Browse the repository at this point in the history
DLP-655 Adds support for DLP profiles
  • Loading branch information
jacobbednarz committed Oct 31, 2022
2 parents 22e0026 + 2ef4109 commit edebff1
Show file tree
Hide file tree
Showing 8 changed files with 587 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
```
109 changes: 109 additions & 0 deletions docs/resources/dlp_profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
page_title: "cloudflare_dlp_profile Resource - Cloudflare"
subcategory: ""
description: |-
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.
---

# cloudflare_dlp_profile (Resource)

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.

## Example Usage

```terraform
# 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"
}
}
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `account_id` (String) The account identifier to target for the resource.
- `entry` (Block Set, Min: 1) List of entries to apply to the profile. (see [below for nested schema](#nestedblock--entry))
- `name` (String) Name of the profile.
- `type` (String) The type of the profile. Available values: `custom`, `predefined`.

### Optional

- `description` (String) Brief summary of the profile and its intended use.

### Read-Only

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

<a id="nestedblock--entry"></a>
### Nested Schema for `entry`

Required:

- `name` (String) Name of the entry to deploy.

Optional:

- `enabled` (Boolean) Whether the entry is active. Defaults to `false`.
- `id` (String) Unique entry identifier.
- `pattern` (Block List, Max: 1) (see [below for nested schema](#nestedblock--entry--pattern))

<a id="nestedblock--entry--pattern"></a>
### Nested Schema for `entry.pattern`

Required:

- `regex` (String) The regex that defines the pattern.

Optional:

- `validation` (String) The validation algorithm to apply with this pattern.

## Import

Import is supported using the following syntax:
```shell
$ terraform import cloudflare_dlp_profile.example <account_id>/<dlp_profile_id>
```
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"
"strings"

"github.com/cloudflare/cloudflare-go"

"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))

d.Set("account_id", accountID)
d.SetId(dlpProfileID)

resourceCloudflareDLPProfileRead(ctx, d, meta)
return []*schema.ResourceData{d}, nil
}

0 comments on commit edebff1

Please sign in to comment.