-
Notifications
You must be signed in to change notification settings - Fork 541
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1984 from GreenStage/egomes/DLP-655
DLP-655 Adds support for DLP profiles
- Loading branch information
Showing
8 changed files
with
587 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:new-resource | ||
cloudflare_dlp_profile | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
$ terraform import cloudflare_dlp_profile.example <account_id>/<dlp_profile_id> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¬FoundError) { | ||
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 | ||
} |
Oops, something went wrong.