From 60aae15e8943ce723963c6c29c8bf13f53dfcc17 Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Mon, 9 Dec 2024 16:23:21 +0100 Subject: [PATCH 1/5] Add content scanning expression resource --- .../content_scanning_expression/model.go | 9 + .../content_scanning_expression/resource.go | 196 ++++++++++++++++++ .../resource_test.go | 104 ++++++++++ .../content_scanning_expression/schema.go | 29 +++ 4 files changed, 338 insertions(+) create mode 100644 internal/framework/service/content_scanning_expression/model.go create mode 100644 internal/framework/service/content_scanning_expression/resource.go create mode 100644 internal/framework/service/content_scanning_expression/resource_test.go create mode 100644 internal/framework/service/content_scanning_expression/schema.go diff --git a/internal/framework/service/content_scanning_expression/model.go b/internal/framework/service/content_scanning_expression/model.go new file mode 100644 index 0000000000..100019c401 --- /dev/null +++ b/internal/framework/service/content_scanning_expression/model.go @@ -0,0 +1,9 @@ +package content_scanning_expression + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type ContentScanningExpressionModel struct { + ZoneID types.String `tfsdk:"zone_id"` + ID types.String `tfsdk:"id"` + Payload types.String `tfsdk:"payload"` +} diff --git a/internal/framework/service/content_scanning_expression/resource.go b/internal/framework/service/content_scanning_expression/resource.go new file mode 100644 index 0000000000..089d9d6ef3 --- /dev/null +++ b/internal/framework/service/content_scanning_expression/resource.go @@ -0,0 +1,196 @@ +package content_scanning_expression + +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 = &ContentScanningExpressionResource{} + _ resource.ResourceWithImportState = &ContentScanningExpressionResource{} +) + +func NewResource() resource.Resource { + return &ContentScanningExpressionResource{} +} + +type ContentScanningExpressionResource struct { + client *muxclient.Client +} + +func (r *ContentScanningExpressionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_content_scanning_expression" +} + +func (r *ContentScanningExpressionResource) 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 *ContentScanningExpressionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ContentScanningExpressionModel + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + params := cloudflare.ContentScanningAddCustomExpressionsParams{ + Payloads: []cloudflare.ContentScanningCustomPayload{ + { + Payload: data.Payload.ValueString(), + }, + }, + } + expressions, err := r.client.V1.ContentScanningAddCustomExpressions(ctx, cloudflare.ZoneIdentifier(data.ZoneID.ValueString()), params) + if err != nil { + resp.Diagnostics.AddError("Error creating a custom scan expression for Content Scanning", err.Error()) + return + } + + // The Add API returns a list of all exiting custom scan expression + // loop until we find the newly created one, matching on payload + // payload uniqueness is enforced by the service + for _, exp := range expressions { + if exp.Payload == data.Payload.ValueString() { + data.ID = types.StringValue(exp.ID) + break + } + } + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r *ContentScanningExpressionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ContentScanningExpressionModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + zoneID := state.ZoneID.ValueString() + var foundExp cloudflare.ContentScanningCustomExpression + expressions, err := r.client.V1.ContentScanningListCustomExpressions(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.ContentScanningListCustomExpressionsParams{}) + if err != nil { + resp.Diagnostics.AddError("Error listing customs scan expressions for Content Scanning", err.Error()) + return + } + + // content scanning doens't offer a single get operation so + // loop until we find the matching ID. + for _, exp := range expressions { + if exp.ID == state.ID.ValueString() { + foundExp = exp + break + } + } + + state.ID = types.StringValue(foundExp.ID) + state.Payload = types.StringValue(foundExp.Payload) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +func (r *ContentScanningExpressionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ContentScanningExpressionModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + var state ContentScanningExpressionModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + zoneID := cloudflare.ZoneIdentifier(plan.ZoneID.ValueString()) + plan.ID = state.ID + + // API does not offer an update operation so we use delete/create + delParams := cloudflare.ContentScanningDeleteCustomExpressionsParams{ID: plan.ID.ValueString()} + _, err := r.client.V1.ContentScanningDeleteCustomExpression(ctx, zoneID, delParams) + if err != nil { + resp.Diagnostics.AddError("Error in Update while deleting custom scan expression for Content Scanning", err.Error()) + return + } + createParams := cloudflare.ContentScanningAddCustomExpressionsParams{ + Payloads: []cloudflare.ContentScanningCustomPayload{ + { + Payload: plan.Payload.ValueString(), + }, + }, + } + expressions, err := r.client.V1.ContentScanningAddCustomExpressions(ctx, zoneID, createParams) + if err != nil { + resp.Diagnostics.AddError("Error in Update while creating a custom scan expression for Content Scanning", err.Error()) + return + } + + // The Add API returns a list of all exiting custom scan expression + // loop until we find the newly created one, matching on payload + // payload uniqueness is enforced by the service + for _, exp := range expressions { + if exp.Payload == plan.Payload.ValueString() { + plan.ID = types.StringValue(exp.ID) + break + } + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +func (r *ContentScanningExpressionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ContentScanningExpressionModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + zoneID := cloudflare.ZoneIdentifier(state.ZoneID.ValueString()) + deleteParam := cloudflare.ContentScanningDeleteCustomExpressionsParams{ID: state.ID.ValueString()} + _, err := r.client.V1.ContentScanningDeleteCustomExpression(ctx, zoneID, deleteParam) + if err != nil { + resp.Diagnostics.AddError("Error deleting custom scan expression for Content Scanning", err.Error()) + return + } +} + +func (r *ContentScanningExpressionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idparts := strings.Split(req.ID, "/") + if len(idparts) != 2 { + resp.Diagnostics.AddError("error importing content scanning custom expression", "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], + )...) +} diff --git a/internal/framework/service/content_scanning_expression/resource_test.go b/internal/framework/service/content_scanning_expression/resource_test.go new file mode 100644 index 0000000000..4b74ec74ca --- /dev/null +++ b/internal/framework/service/content_scanning_expression/resource_test.go @@ -0,0 +1,104 @@ +package content_scanning_expression_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_content_scanning_expression", &resource.Sweeper{ + Name: "cloudflare_content_scanning_expression", + F: testSweepCloudflareCSExpression, + }) +} + +func testSweepCloudflareCSExpression(r string) error { + ctx := context.Background() + client, clientErr := acctest.SharedV1Client() + if clientErr != nil { + tflog.Error(ctx, fmt.Sprintf("Sweeper: 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 expressions from API + expressions, err := client.ContentScanningListCustomExpressions(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.ContentScanningListCustomExpressionsParams{}) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Sweeper: error listing customs scan expressions for Content Scanning: %s", err)) + return err + } + for _, exp := range expressions { + deleteParam := cfv1.ContentScanningDeleteCustomExpressionsParams{ID: exp.ID} + _, err := client.ContentScanningDeleteCustomExpression(ctx, cfv1.ZoneIdentifier(zoneID), deleteParam) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Sweeper: error deleting custom scan expression for Content Scanning: %s", err)) + } + } + + return nil +} + +func TestAccCloudflareContentScanningExpression_Basic(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + name := fmt.Sprintf("cloudflare_content_scanning_expression.%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: testAccBasicConfig(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name+"_first", "zone_id", zoneID), + resource.TestCheckResourceAttr(name+"_first", "payload", "lookup_json_string(http.request.body.raw, \"file\")"), + + resource.TestCheckResourceAttr(name+"_second", "zone_id", zoneID), + resource.TestCheckResourceAttr(name+"_second", "payload", "lookup_json_string(http.request.body.raw, \"document\")"), + ), + }, + { + Config: testAccBasicConfigChange(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name+"_second", "zone_id", zoneID), + resource.TestCheckResourceAttr(name+"_second", "payload", "lookup_json_string(http.request.body.raw, \"txt\")"), + ), + }, + }, + }) +} + +func testAccBasicConfig(name, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_content_scanning_expression" "%[1]s_first" { + zone_id = "%[2]s" + payload = "lookup_json_string(http.request.body.raw, \"file\")" + } + + resource "cloudflare_content_scanning_expression" "%[1]s_second" { + zone_id = "%[2]s" + payload = "lookup_json_string(http.request.body.raw, \"document\")" + }`, name, zoneID) +} + +func testAccBasicConfigChange(name, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_content_scanning_expression" "%[1]s_second" { + zone_id = "%[2]s" + payload = "lookup_json_string(http.request.body.raw, \"txt\")" + }`, name, zoneID) +} diff --git a/internal/framework/service/content_scanning_expression/schema.go b/internal/framework/service/content_scanning_expression/schema.go new file mode 100644 index 0000000000..398426d18b --- /dev/null +++ b/internal/framework/service/content_scanning_expression/schema.go @@ -0,0 +1,29 @@ +package content_scanning_expression + +import ( + "context" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func (r *ContentScanningExpressionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provides a Cloudflare Content Scanning Expression resource for managing custom scan expression within a specific zone.", + Attributes: map[string]schema.Attribute{ + consts.IDSchemaKey: schema.StringAttribute{ + Description: consts.IDSchemaDescription, + Computed: true, + }, + consts.ZoneIDSchemaKey: schema.StringAttribute{ + Description: consts.ZoneIDSchemaDescription, + Required: true, + }, + "payload": schema.StringAttribute{ + Description: "Custom scan expression to tell the content scanner where to find the content objects.", + Required: true, + }, + }, + } +} From 0092caaa934741a3f0f8469aa60ea5df5ac4d7fe Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Mon, 9 Dec 2024 16:24:09 +0100 Subject: [PATCH 2/5] Add content scanning expression examples --- .../import.sh | 1 + .../resource.tf | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 examples/resources/cloudflare_content_scanning_expression/import.sh create mode 100644 examples/resources/cloudflare_content_scanning_expression/resource.tf diff --git a/examples/resources/cloudflare_content_scanning_expression/import.sh b/examples/resources/cloudflare_content_scanning_expression/import.sh new file mode 100644 index 0000000000..3591b3ef76 --- /dev/null +++ b/examples/resources/cloudflare_content_scanning_expression/import.sh @@ -0,0 +1 @@ +terraform import cloudflare_content_scanning_expression.example / diff --git a/examples/resources/cloudflare_content_scanning_expression/resource.tf b/examples/resources/cloudflare_content_scanning_expression/resource.tf new file mode 100644 index 0000000000..3faf65ca79 --- /dev/null +++ b/examples/resources/cloudflare_content_scanning_expression/resource.tf @@ -0,0 +1,15 @@ +# Enable Content Scanning before trying to add custom scan expressions +resource "cloudflare_content_scanning" "example" { + zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b" + enabled = true +} + +resource "cloudflare_content_scanning_expression" "first_example" { + zone_id = cloudflare_content_scanning.example.zone_id + payload = "lookup_json_string(http.request.body.raw, \"file\")" +} + +resource "cloudflare_content_scanning_expression" "second_example" { + zone_id = cloudflare_content_scanning.example.zone_id + payload = "lookup_json_string(http.request.body.raw, \"document\")" +} From 1d90f26ef220fa7f0a0c3d4d28e8febd75da789d Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Mon, 9 Dec 2024 16:24:29 +0100 Subject: [PATCH 3/5] Add content scanning expression to provider --- internal/framework/provider/provider.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 5330e91b58..b38a367b93 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -18,6 +18,7 @@ import ( "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/access_mutual_tls_hostname_settings" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/api_token_permissions_groups" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/cloud_connector_rules" + "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/content_scanning_expression" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/d1" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/dcv_delegation" "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/dlp_datasets" @@ -395,6 +396,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re zero_trust_infrastructure_access_target.NewResource, leaked_credential_check.NewResource, leaked_credential_check_rule.NewResource, + content_scanning_expression.NewResource, } } From 5bfc95022b387d9bdf7df06becb07397efa4dd8f Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Mon, 9 Dec 2024 16:24:39 +0100 Subject: [PATCH 4/5] `make docs` --- docs/resources/content_scanning_expression.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/resources/content_scanning_expression.md diff --git a/docs/resources/content_scanning_expression.md b/docs/resources/content_scanning_expression.md new file mode 100644 index 0000000000..0828c46b66 --- /dev/null +++ b/docs/resources/content_scanning_expression.md @@ -0,0 +1,49 @@ +--- +page_title: "cloudflare_content_scanning_expression Resource - Cloudflare" +subcategory: "" +description: |- + Provides a Cloudflare Content Scanning Expression resource for managing custom scan expression within a specific zone. +--- + +# cloudflare_content_scanning_expression (Resource) + +Provides a Cloudflare Content Scanning Expression resource for managing custom scan expression within a specific zone. + +## Example Usage + +```terraform +# Enable Content Scanning before trying to add custom scan expressions +resource "cloudflare_content_scanning" "example" { + zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b" + enabled = true +} + +resource "cloudflare_content_scanning_expression" "first_example" { + zone_id = cloudflare_content_scanning.example.zone_id + payload = "lookup_json_string(http.request.body.raw, \"file\")" +} + +resource "cloudflare_content_scanning_expression" "second_example" { + zone_id = cloudflare_content_scanning.example.zone_id + payload = "lookup_json_string(http.request.body.raw, \"document\")" +} +``` + +## Schema + +### Required + +- `payload` (String) Custom scan expression to tell the content scanner where to find the content objects. +- `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_content_scanning_expression.example / +``` From c3048c6f1d2a975465eadacd3a9ae178830d9194 Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Mon, 9 Dec 2024 16:30:32 +0100 Subject: [PATCH 5/5] Add changelog --- .changelog/4734.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/4734.txt diff --git a/.changelog/4734.txt b/.changelog/4734.txt new file mode 100644 index 0000000000..223d86cc92 --- /dev/null +++ b/.changelog/4734.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_content_scanning_expression +``` \ No newline at end of file