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 diff --git a/.changelog/4743.txt b/.changelog/4743.txt new file mode 100644 index 0000000000..5d6ea14b94 --- /dev/null +++ b/.changelog/4743.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/access_application: support multi-valued + Access service token authentication for SCIM provisioning to Access applications +``` diff --git a/.changelog/4814.txt b/.changelog/4814.txt new file mode 100644 index 0000000000..d037303bca --- /dev/null +++ b/.changelog/4814.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/cloudflare_ruleset: handle when `disable_stale_while_updating` is an empty object but not nil +``` diff --git a/.changelog/4817.txt b/.changelog/4817.txt new file mode 100644 index 0000000000..1405728ab7 --- /dev/null +++ b/.changelog/4817.txt @@ -0,0 +1,3 @@ +```release-note:note +resource/cloudflare_teams_location: remove unusable `policy_ids` attribute +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a986f4ff..2fb4638ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,30 @@ -## 4.49.0 (Unreleased) +## 4.50.0 (Unreleased) + +## 4.49.0 (December 25th, 2025) + +NOTES: + +* resource/cloudflare_teams_location: remove unusable `policy_ids` attribute ([#4817](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4817)) + +FEATURES: + +* **New Resource:** `cloudflare_content_scanning_expression` ([#4734](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4734)) + +ENHANCEMENTS: + +* resource/access_application: support multi-valued + Access service token authentication for SCIM provisioning to Access applications ([#4743](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4743)) + +BUG FIXES: + +* resource/cloudflare_ruleset: handle when `disable_stale_while_updating` is an empty object but not nil ([#4814](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4814)) + +DEPENDENCIES: + +* provider: bump github.com/cloudflare/cloudflare-go from 0.111.0 to 0.112.0 ([#4803](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4803)) +* provider: bump github.com/hashicorp/terraform-plugin-framework-validators from 0.15.0 to 0.16.0 ([#4762](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4762)) +* provider: bump golang.org/x/crypto from 0.21.0 to 0.31.0 in /tools ([#4755](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4755)) +* provider: bump golang.org/x/crypto from 0.30.0 to 0.31.0 ([#4756](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4756)) +* provider: bump golang.org/x/net from 0.32.0 to 0.33.0 ([#4802](https://github.com/cloudflare/terraform-provider-cloudflare/issues/4802)) ## 4.48.0 (December 11th, 2024) diff --git a/docs/resources/access_application.md b/docs/resources/access_application.md index f86241e53d..e481556f54 100644 --- a/docs/resources/access_application.md +++ b/docs/resources/access_application.md @@ -281,7 +281,7 @@ Required: Optional: -- `authentication` (Block List, Max: 1) Attributes for configuring HTTP Basic, OAuth Bearer token, or OAuth 2 authentication schemes for SCIM provisioning to an application. (see [below for nested schema](#nestedblock--scim_config--authentication)) +- `authentication` (Block List) Attributes for configuring HTTP Basic, OAuth Bearer token, or OAuth 2 authentication schemes for SCIM provisioning to an application. (see [below for nested schema](#nestedblock--scim_config--authentication)) - `deactivate_on_delete` (Boolean) If false, propagates DELETE requests to the target application for SCIM resources. If true, sets 'active' to false on the SCIM resource. Note: Some targets do not support DELETE operations. - `enabled` (Boolean) Whether SCIM provisioning is turned on for this application. - `mappings` (Block List) A list of mappings to apply to SCIM resources before provisioning them in this application. These can transform or filter the resources to be provisioned. (see [below for nested schema](#nestedblock--scim_config--mappings)) @@ -295,14 +295,14 @@ Required: Optional: -- `authorization_url` (String) URL used to generate the auth code used during token generation. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `client_id` (String) Client ID used to authenticate when generating a token for authenticating with the remote SCIM service. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `client_secret` (String) Secret used to authenticate when generating a token for authenticating with the remove SCIM service. Required when using `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `password` (String) Required when using `scim_config.0.authentication.0.user`. Conflicts with `scim_config.0.authentication.0.token`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. -- `scopes` (Set of String) The authorization scopes to request when generating the token used to authenticate with the remove SCIM service. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `token` (String) Token used to authenticate with the remote SCIM service. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. -- `token_url` (String) URL used to generate the token used to authenticate with the remote SCIM service. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.client_id`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `user` (String) User name used to authenticate with the remote SCIM service. Required when using `scim_config.0.authentication.0.password`. Conflicts with `scim_config.0.authentication.0.token`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. +- `authorization_url` (String) URL used to generate the auth code used during token generation. +- `client_id` (String) Client ID used to authenticate when generating a token for authenticating with the remote SCIM service. +- `client_secret` (String) Secret used to authenticate when generating a token for authenticating with the remove SCIM service. +- `password` (String) +- `scopes` (Set of String) The authorization scopes to request when generating the token used to authenticate with the remove SCIM service. +- `token` (String) Token used to authenticate with the remote SCIM service. +- `token_url` (String) URL used to generate the token used to authenticate with the remote SCIM service. +- `user` (String) User name used to authenticate with the remote SCIM service. diff --git a/docs/resources/cloud_connector_rules.md b/docs/resources/cloud_connector_rules.md index e4d2ee0cae..49f53fbf13 100644 --- a/docs/resources/cloud_connector_rules.md +++ b/docs/resources/cloud_connector_rules.md @@ -2,7 +2,7 @@ page_title: "cloudflare_cloud_connector_rules Resource - Cloudflare" subcategory: "" description: |- - The Cloud Connector Rules resource allows you to create and manage cloud connector rules for a zone. + The Cloud Connector Rules https://developers.cloudflare.com/rules/cloud-connector/ resource allows you to create and manage cloud connector rules for a zone. --- # cloudflare_cloud_connector_rules (Resource) 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 / +``` diff --git a/docs/resources/teams_location.md b/docs/resources/teams_location.md index 7574ed4e25..fa7cf986c2 100644 --- a/docs/resources/teams_location.md +++ b/docs/resources/teams_location.md @@ -50,7 +50,6 @@ resource "cloudflare_teams_location" "example" { - `id` (String) The ID of this resource. - `ip` (String) Client IP address. - `ipv4_destination` (String) IP to direct all IPv4 DNS queries to. -- `policy_ids` (List of String) ### Nested Schema for `networks` diff --git a/docs/resources/zero_trust_access_application.md b/docs/resources/zero_trust_access_application.md index c2762f49c6..1c508626fd 100644 --- a/docs/resources/zero_trust_access_application.md +++ b/docs/resources/zero_trust_access_application.md @@ -262,7 +262,7 @@ Required: Optional: -- `authentication` (Block List, Max: 1) Attributes for configuring HTTP Basic, OAuth Bearer token, or OAuth 2 authentication schemes for SCIM provisioning to an application. (see [below for nested schema](#nestedblock--scim_config--authentication)) +- `authentication` (Block List) Attributes for configuring HTTP Basic, OAuth Bearer token, or OAuth 2 authentication schemes for SCIM provisioning to an application. (see [below for nested schema](#nestedblock--scim_config--authentication)) - `deactivate_on_delete` (Boolean) If false, propagates DELETE requests to the target application for SCIM resources. If true, sets 'active' to false on the SCIM resource. Note: Some targets do not support DELETE operations. - `enabled` (Boolean) Whether SCIM provisioning is turned on for this application. - `mappings` (Block List) A list of mappings to apply to SCIM resources before provisioning them in this application. These can transform or filter the resources to be provisioned. (see [below for nested schema](#nestedblock--scim_config--mappings)) @@ -276,14 +276,14 @@ Required: Optional: -- `authorization_url` (String) URL used to generate the auth code used during token generation. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `client_id` (String) Client ID used to authenticate when generating a token for authenticating with the remote SCIM service. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `client_secret` (String) Secret used to authenticate when generating a token for authenticating with the remove SCIM service. Required when using `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `password` (String) Required when using `scim_config.0.authentication.0.user`. Conflicts with `scim_config.0.authentication.0.token`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. -- `scopes` (Set of String) The authorization scopes to request when generating the token used to authenticate with the remove SCIM service. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `token` (String) Token used to authenticate with the remote SCIM service. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. -- `token_url` (String) URL used to generate the token used to authenticate with the remote SCIM service. Required when using `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.client_id`. Conflicts with `scim_config.0.authentication.0.user`, `scim_config.0.authentication.0.password`, `scim_config.0.authentication.0.token`. -- `user` (String) User name used to authenticate with the remote SCIM service. Required when using `scim_config.0.authentication.0.password`. Conflicts with `scim_config.0.authentication.0.token`, `scim_config.0.authentication.0.client_id`, `scim_config.0.authentication.0.client_secret`, `scim_config.0.authentication.0.authorization_url`, `scim_config.0.authentication.0.token_url`, `scim_config.0.authentication.0.scopes`. +- `authorization_url` (String) URL used to generate the auth code used during token generation. +- `client_id` (String) Client ID used to authenticate when generating a token for authenticating with the remote SCIM service. +- `client_secret` (String) Secret used to authenticate when generating a token for authenticating with the remove SCIM service. +- `password` (String) +- `scopes` (Set of String) The authorization scopes to request when generating the token used to authenticate with the remove SCIM service. +- `token` (String) Token used to authenticate with the remote SCIM service. +- `token_url` (String) URL used to generate the token used to authenticate with the remote SCIM service. +- `user` (String) User name used to authenticate with the remote SCIM service. diff --git a/docs/resources/zero_trust_dns_location.md b/docs/resources/zero_trust_dns_location.md index 7d8d806280..1b71ebc83b 100644 --- a/docs/resources/zero_trust_dns_location.md +++ b/docs/resources/zero_trust_dns_location.md @@ -50,7 +50,6 @@ resource "cloudflare_zero_trust_dns_location" "example" { - `id` (String) The ID of this resource. - `ip` (String) Client IP address. - `ipv4_destination` (String) IP to direct all IPv4 DNS queries to. -- `policy_ids` (List of String) ### Nested Schema for `networks` 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\")" +} diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 97561762a1..1b8247356b 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -19,6 +19,7 @@ import ( "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" + "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" @@ -397,6 +398,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re leaked_credential_check.NewResource, leaked_credential_check_rule.NewResource, content_scanning.NewResource, + content_scanning_expression.NewResource, } } 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, + }, + }, + } +} diff --git a/internal/framework/service/rulesets/resource.go b/internal/framework/service/rulesets/resource.go index 84902ef3dc..6de78078a0 100644 --- a/internal/framework/service/rulesets/resource.go +++ b/internal/framework/service/rulesets/resource.go @@ -623,9 +623,11 @@ func toRulesetResourceModel(ctx context.Context, zoneID, accountID basetypes.Str } if ruleResponse.ActionParameters.ServeStale != nil { - rule.ActionParameters[0].ServeStale = []*ActionParameterServeStaleModel{{ - DisableStaleWhileUpdating: types.BoolValue(*ruleResponse.ActionParameters.ServeStale.DisableStaleWhileUpdating), - }} + if ruleResponse.ActionParameters.ServeStale.DisableStaleWhileUpdating != nil { + rule.ActionParameters[0].ServeStale = []*ActionParameterServeStaleModel{{ + DisableStaleWhileUpdating: types.BoolValue(*ruleResponse.ActionParameters.ServeStale.DisableStaleWhileUpdating), + }} + } } if ruleResponse.ActionParameters.FromList != nil { diff --git a/internal/sdkv2provider/resource_cloudflare_access_application_test.go b/internal/sdkv2provider/resource_cloudflare_access_application_test.go index d220fe29e2..64b43713c6 100644 --- a/internal/sdkv2provider/resource_cloudflare_access_application_test.go +++ b/internal/sdkv2provider/resource_cloudflare_access_application_test.go @@ -255,8 +255,10 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigInvalidMappingSchema(t *te }) } -func TestAccCloudflareAccessApplication_WithSCIMConfigHttpBasicMissingRequired(t *testing.T) { +func TestAccCloudflareAccessApplication_WithSCIMConfigOAuthBearerToken(t *testing.T) { rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd) + idpName := fmt.Sprintf("cloudflare_zero_trust_access_identity_provider.%s", rnd) resource.Test(t, resource.TestCase{ PreCheck: func() { @@ -266,14 +268,34 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigHttpBasicMissingRequired(t CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy, Steps: []resource.TestStep{ { - Config: testAccCloudflareAccessApplicationSCIMConfigHttpBasicMissingRequired(rnd, accountID, domain), - ExpectError: regexp.MustCompile(`.*all of\s*` + "`scim_config.0.authentication.0.password,scim_config.0.authentication.0.user`" + `\s*must be specified.*`), + Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuthBearerToken(rnd, accountID, domain), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), + resource.TestCheckResourceAttr(name, "name", rnd), + resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)), + resource.TestCheckResourceAttr(name, "type", "self_hosted"), + resource.TestCheckResourceAttr(name, "session_duration", "24h"), + resource.TestCheckResourceAttr(name, "scim_config.#", "1"), + resource.TestCheckResourceAttr(name, "scim_config.0.enabled", "true"), + resource.TestCheckResourceAttr(name, "scim_config.0.remote_uri", "scim.com"), + resource.TestCheckResourceAttrPair(name, "scim_config.0.idp_uid", idpName, "id"), + resource.TestCheckResourceAttr(name, "scim_config.0.deactivate_on_delete", "true"), + resource.TestCheckResourceAttr(name, "scim_config.0.authentication.0.scheme", "oauthbearertoken"), + resource.TestCheckResourceAttrSet(name, "scim_config.0.authentication.0.token"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.schema", "urn:ietf:params:scim:schemas:core:2.0:User"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.enabled", "true"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.filter", "title pr or userType eq \"Intern\""), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.transform_jsonata", "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.operations.0.create", "true"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.operations.0.update", "true"), + resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.operations.0.delete", "true"), + ), }, }, }) } -func TestAccCloudflareAccessApplication_WithSCIMConfigOAuthBearerToken(t *testing.T) { +func TestAccCloudflareAccessApplication_WithSCIMConfigMultiAuth(t *testing.T) { rnd := generateRandomResourceName() name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd) idpName := fmt.Sprintf("cloudflare_zero_trust_access_identity_provider.%s", rnd) @@ -286,7 +308,7 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigOAuthBearerToken(t *testin CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy, Steps: []resource.TestStep{ { - Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuthBearerToken(rnd, accountID, domain), + Config: testAccCloudflareAccessApplicationSCIMConfigValidMulti(rnd, accountID, domain), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), resource.TestCheckResourceAttr(name, "name", rnd), @@ -300,6 +322,9 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigOAuthBearerToken(t *testin resource.TestCheckResourceAttr(name, "scim_config.0.deactivate_on_delete", "true"), resource.TestCheckResourceAttr(name, "scim_config.0.authentication.0.scheme", "oauthbearertoken"), resource.TestCheckResourceAttrSet(name, "scim_config.0.authentication.0.token"), + resource.TestCheckResourceAttr(name, "scim_config.0.authentication.1.scheme", "access_service_token"), + resource.TestCheckResourceAttr(name, "scim_config.0.authentication.1.client_id", "1234"), + resource.TestCheckResourceAttrSet(name, "scim_config.0.authentication.1.client_secret"), resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.schema", "urn:ietf:params:scim:schemas:core:2.0:User"), resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.enabled", "true"), resource.TestCheckResourceAttr(name, "scim_config.0.mappings.0.filter", "title pr or userType eq \"Intern\""), @@ -358,42 +383,6 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigOAuth2(t *testing.T) { }) } -func TestAccCloudflareAccessApplication_WithSCIMConfigOAuth2MissingRequired(t *testing.T) { - rnd := generateRandomResourceName() - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - ProviderFactories: providerFactories, - CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCloudflareAccessApplicationSCIMConfigOAuth2MissingRequired(rnd, accountID, domain), - ExpectError: regexp.MustCompile(`.*all of\s*` + "`scim_config.0.authentication.0.authorization_url,scim_config.0.authentication.0.client_id,scim_config.0.authentication.0.client_secret,scim_config.0.authentication.0.token_url`" + `\s*must be specified.*`), - }, - }, - }) -} - -func TestAccCloudflareAccessApplication_WithSCIMConfigAuthenticationInvalid(t *testing.T) { - rnd := generateRandomResourceName() - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - ProviderFactories: providerFactories, - CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCloudflareAccessApplicationSCIMConfigAuthenticationInvalid(rnd, accountID, domain), - ExpectError: regexp.MustCompile(`Error: Conflicting configuration arguments`), - }, - }, - }) -} - func TestAccCloudflareAccessApplication_WithCORS(t *testing.T) { rnd := generateRandomResourceName() name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd) @@ -1983,7 +1972,7 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" { `, rnd, accountID, domain) } -func testAccCloudflareAccessApplicationSCIMConfigValidOAuth2(rnd, accountID, domain string) string { +func testAccCloudflareAccessApplicationSCIMConfigValidMulti(rnd, accountID, domain string) string { return fmt.Sprintf(` resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" { account_id = "%[2]s" @@ -2015,66 +2004,13 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" { idp_uid = cloudflare_zero_trust_access_identity_provider.%[1]s.id deactivate_on_delete = true authentication { - scheme = "oauth2" - client_id = "beepboop" - client_secret = "bop" - authorization_url = "https://www.authorization.com" - token_url = "https://www.token.com" - scopes = ["read"] - } - mappings { - schema = "urn:ietf:params:scim:schemas:core:2.0:User" - enabled = true - filter = "title pr or userType eq \"Intern\"" - transform_jsonata = "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])" - operations { - create = true - update = true - delete = true - } - } - } -} -`, rnd, accountID, domain) -} - -func testAccCloudflareAccessApplicationSCIMConfigOAuth2MissingRequired(rnd, accountID, domain string) string { - return fmt.Sprintf(` -resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" { - account_id = "%[2]s" - name = "%[1]s" - type = "azureAD" - config { - client_id = "test" - client_secret = "test" - directory_id = "directory" - support_groups = true - } - scim_config { - enabled = true - group_member_deprovision = true - seat_deprovision = true - user_deprovision = true + scheme = "oauthbearertoken" + token = "beepboop" } -} - -resource "cloudflare_zero_trust_access_application" "%[1]s" { - account_id = "%[2]s" - name = "%[1]s" - type = "self_hosted" - session_duration = "24h" - domain = "%[1]s.%[3]s" - scim_config { - enabled = true - remote_uri = "scim.com" - idp_uid = cloudflare_zero_trust_access_identity_provider.%[1]s.id - deactivate_on_delete = true authentication { - scheme = "oauth2" - client_id = "beepboop" - client_secret = "bop" - authorization_url = "https://www.authorization.com" - scopes = ["read"] + scheme = "access_service_token" + client_id = "1234" + client_secret = "5678" } mappings { schema = "urn:ietf:params:scim:schemas:core:2.0:User" @@ -2086,13 +2022,14 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" { update = true delete = true } + strictness = "strict" } } } `, rnd, accountID, domain) } -func testAccCloudflareAccessApplicationSCIMConfigAuthenticationInvalid(rnd, accountID, domain string) string { +func testAccCloudflareAccessApplicationSCIMConfigValidOAuth2(rnd, accountID, domain string) string { return fmt.Sprintf(` resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" { account_id = "%[2]s" @@ -2129,9 +2066,6 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" { client_secret = "bop" authorization_url = "https://www.authorization.com" token_url = "https://www.token.com" - token = "1234" - user = "user" - password = "ahhh" scopes = ["read"] } mappings { @@ -2150,56 +2084,6 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" { `, rnd, accountID, domain) } -func testAccCloudflareAccessApplicationSCIMConfigHttpBasicMissingRequired(rnd, accountID, domain string) string { - return fmt.Sprintf(` -resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" { - account_id = "%[2]s" - name = "%[1]s" - type = "azureAD" - config { - client_id = "test" - client_secret = "test" - directory_id = "directory" - support_groups = true - } - scim_config { - enabled = true - group_member_deprovision = true - seat_deprovision = true - user_deprovision = true - } -} - -resource "cloudflare_zero_trust_access_application" "%[1]s" { - account_id = "%[2]s" - name = "%[1]s" - type = "self_hosted" - session_duration = "24h" - domain = "%[1]s.%[3]s" - scim_config { - enabled = true - remote_uri = "scim.com" - idp_uid = cloudflare_zero_trust_access_identity_provider.%[1]s.id - deactivate_on_delete = true - authentication { - scheme = "httpbasic" - user = "test" - } - mappings { - schema = "urn:ietf:params:scim:schemas:core:2.0:User" - enabled = true - filter = "title pr or userType eq \"Intern\"" - transform_jsonata = "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])" - operations { - create = true - update = true - delete = true - } - } - } -}`, rnd, accountID, domain) -} - func testAccCloudflareAccessApplicationSCIMConfigInvalidMappingSchema(rnd, accountID, domain string) string { return fmt.Sprintf(` resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" { diff --git a/internal/sdkv2provider/resource_cloudflare_teams_location.go b/internal/sdkv2provider/resource_cloudflare_teams_location.go index 8fdb131eb1..1ca7fdef4c 100644 --- a/internal/sdkv2provider/resource_cloudflare_teams_location.go +++ b/internal/sdkv2provider/resource_cloudflare_teams_location.go @@ -68,9 +68,6 @@ func resourceCloudflareTeamsLocationRead(ctx context.Context, d *schema.Resource if err := d.Set("networks", flattenTeamsLocationNetworks(location.Networks)); err != nil { return diag.FromErr(fmt.Errorf("error parsing Location networks")) } - if err := d.Set("policy_ids", location.PolicyIDs); err != nil { - return diag.FromErr(fmt.Errorf("error parsing Location policy IDs")) - } if err := d.Set("ip", location.Ip); err != nil { return diag.FromErr(fmt.Errorf("error parsing Location IP")) } diff --git a/internal/sdkv2provider/schema_cloudflare_access_application.go b/internal/sdkv2provider/schema_cloudflare_access_application.go index e5e8967b8d..684e7ffa76 100644 --- a/internal/sdkv2provider/schema_cloudflare_access_application.go +++ b/internal/sdkv2provider/schema_cloudflare_access_application.go @@ -706,80 +706,65 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema { Type: schema.TypeList, Optional: true, Description: "Attributes for configuring HTTP Basic, OAuth Bearer token, or OAuth 2 authentication schemes for SCIM provisioning to an application.", - MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ // Common Attributes "scheme": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringInSlice([]string{"httpbasic", "oauthbearertoken", "oauth2"}, false), + ValidateFunc: validation.StringInSlice([]string{"httpbasic", "oauthbearertoken", "oauth2", "access_service_token"}, false), Description: "The authentication scheme to use when making SCIM requests to this application.", }, // HTTP Basic Authentication Attributes "user": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"scim_config.0.authentication.0.password"}, - ConflictsWith: []string{"scim_config.0.authentication.0.token", "scim_config.0.authentication.0.client_id", "scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.token_url", "scim_config.0.authentication.0.scopes"}, - Description: "User name used to authenticate with the remote SCIM service.", + Type: schema.TypeString, + Optional: true, + Description: "User name used to authenticate with the remote SCIM service.", }, "password": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"scim_config.0.authentication.0.user"}, - ConflictsWith: []string{"scim_config.0.authentication.0.token", "scim_config.0.authentication.0.client_id", "scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.token_url", "scim_config.0.authentication.0.scopes"}, + Type: schema.TypeString, + Optional: true, StateFunc: func(val interface{}) string { return CONCEALED_STRING }, }, // OAuth Bearer Token Authentication Attributes "token": { - Type: schema.TypeString, - Optional: true, - Description: "Token used to authenticate with the remote SCIM service.", - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.client_id", "scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.token_url", "scim_config.0.authentication.0.scopes"}, + Type: schema.TypeString, + Optional: true, + Description: "Token used to authenticate with the remote SCIM service.", StateFunc: func(val interface{}) string { return CONCEALED_STRING }, }, // OAuth 2 Authentication Attributes "client_id": { - Type: schema.TypeString, - Optional: true, - Description: "Client ID used to authenticate when generating a token for authenticating with the remote SCIM service.", - RequiredWith: []string{"scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.token_url"}, - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.token"}, + Type: schema.TypeString, + Optional: true, + Description: "Client ID used to authenticate when generating a token for authenticating with the remote SCIM service.", }, "client_secret": { - Type: schema.TypeString, - Optional: true, - Description: "Secret used to authenticate when generating a token for authenticating with the remove SCIM service.", - RequiredWith: []string{"scim_config.0.authentication.0.client_id", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.token_url"}, - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.token"}, + Type: schema.TypeString, + Optional: true, + Description: "Secret used to authenticate when generating a token for authenticating with the remove SCIM service.", StateFunc: func(val interface{}) string { return CONCEALED_STRING }, }, "authorization_url": { - Type: schema.TypeString, - Optional: true, - Description: "URL used to generate the auth code used during token generation.", - RequiredWith: []string{"scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.client_id", "scim_config.0.authentication.0.token_url"}, - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.token"}, + Type: schema.TypeString, + Optional: true, + Description: "URL used to generate the auth code used during token generation.", }, "token_url": { - Type: schema.TypeString, - Optional: true, - Description: "URL used to generate the token used to authenticate with the remote SCIM service.", - RequiredWith: []string{"scim_config.0.authentication.0.client_secret", "scim_config.0.authentication.0.authorization_url", "scim_config.0.authentication.0.client_id"}, - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.token"}, + Type: schema.TypeString, + Optional: true, + Description: "URL used to generate the token used to authenticate with the remote SCIM service.", }, "scopes": { - Type: schema.TypeSet, - Description: "The authorization scopes to request when generating the token used to authenticate with the remove SCIM service.", - Optional: true, - ConflictsWith: []string{"scim_config.0.authentication.0.user", "scim_config.0.authentication.0.password", "scim_config.0.authentication.0.token"}, + Type: schema.TypeSet, + Description: "The authorization scopes to request when generating the token used to authenticate with the remove SCIM service.", + Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, }, @@ -1210,37 +1195,48 @@ func convertScimConfigMappingsSchemaToStruct(mappingData map[string]interface{}) func convertScimConfigAuthenticationSchemaToStruct(d *schema.ResourceData) *cloudflare.AccessApplicationScimAuthenticationJson { auth := new(cloudflare.AccessApplicationScimAuthenticationJson) - - if _, ok := d.GetOk("scim_config.0.authentication"); ok { - scheme := cloudflare.AccessApplicationScimAuthenticationScheme(d.Get("scim_config.0.authentication.0.scheme").(string)) + multi := new(cloudflare.AccessApplicationMultipleScimAuthentication) + auth.Value = multi + + keyFmt := "scim_config.0.authentication.%d" + i := 0 + for _, ok := d.GetOk(fmt.Sprintf(keyFmt, i)); ok; _, ok = d.GetOk(fmt.Sprintf(keyFmt, i)) { + key := fmt.Sprintf(keyFmt, i) + scheme := cloudflare.AccessApplicationScimAuthenticationScheme(d.Get(key + ".scheme").(string)) switch scheme { case cloudflare.AccessApplicationScimAuthenticationSchemeHttpBasic: base := &cloudflare.AccessApplicationScimAuthenticationHttpBasic{ - User: d.Get("scim_config.0.authentication.0.user").(string), - Password: d.Get("scim_config.0.authentication.0.password").(string), + User: d.Get(key + ".user").(string), + Password: d.Get(key + ".password").(string), } base.Scheme = scheme - auth.Value = base - break + *multi = append(*multi, &cloudflare.AccessApplicationScimAuthenticationSingleJSON{Value: base}) case cloudflare.AccessApplicationScimAuthenticationSchemeOauthBearerToken: base := &cloudflare.AccessApplicationScimAuthenticationOauthBearerToken{ - Token: d.Get("scim_config.0.authentication.0.token").(string), + Token: d.Get(key + ".token").(string), } base.Scheme = scheme - auth.Value = base - break + *multi = append(*multi, &cloudflare.AccessApplicationScimAuthenticationSingleJSON{Value: base}) case cloudflare.AccessApplicationScimAuthenticationSchemeOauth2: base := &cloudflare.AccessApplicationScimAuthenticationOauth2{ - ClientID: d.Get("scim_config.0.authentication.0.client_id").(string), - ClientSecret: d.Get("scim_config.0.authentication.0.client_secret").(string), - AuthorizationURL: d.Get("scim_config.0.authentication.0.authorization_url").(string), - TokenURL: d.Get("scim_config.0.authentication.0.token_url").(string), - Scopes: expandInterfaceToStringList(d.Get("scim_config.0.authentication.0.scopes").(*schema.Set).List()), + ClientID: d.Get(key + ".client_id").(string), + ClientSecret: d.Get(key + ".client_secret").(string), + AuthorizationURL: d.Get(key + ".authorization_url").(string), + TokenURL: d.Get(key + ".token_url").(string), + Scopes: expandInterfaceToStringList(d.Get(key + ".scopes").(*schema.Set).List()), + } + base.Scheme = scheme + *multi = append(*multi, &cloudflare.AccessApplicationScimAuthenticationSingleJSON{Value: base}) + case cloudflare.AccessApplicationScimAuthenticationAccessServiceToken: + base := &cloudflare.AccessApplicationScimAuthenticationServiceToken{ + ClientID: d.Get(key + ".client_id").(string), + ClientSecret: d.Get(key + ".client_secret").(string), } base.Scheme = scheme - auth.Value = base - break + *multi = append(*multi, &cloudflare.AccessApplicationScimAuthenticationSingleJSON{Value: base}) } + + i++ } return auth @@ -1445,12 +1441,13 @@ func convertScimConfigStructToSchema(scimConfig *cloudflare.AccessApplicationSCI return []interface{}{} } + auth := convertScimConfigAuthenticationStructToSchema(scimConfig.Authentication) config := map[string]interface{}{ "enabled": scimConfig.Enabled, "remote_uri": scimConfig.RemoteURI, "idp_uid": scimConfig.IdPUID, "deactivate_on_delete": cloudflare.Bool(scimConfig.DeactivateOnDelete), - "authentication": convertScimConfigAuthenticationStructToSchema(scimConfig.Authentication), + "authentication": auth, "mappings": convertScimConfigMappingsStructsToSchema(scimConfig.Mappings), } @@ -1464,11 +1461,17 @@ func convertScimConfigAuthenticationStructToSchema(scimAuth *cloudflare.AccessAp auth := map[string]interface{}{} switch t := scimAuth.Value.(type) { + case *cloudflare.AccessApplicationMultipleScimAuthentication: + vals := []interface{}{} + for _, authn := range *t { + vals = append(vals, convertScimConfigSingleAuthentiationToSchema(&cloudflare.AccessApplicationScimAuthenticationJson{Value: authn.Value})) + } + + return vals case *cloudflare.AccessApplicationScimAuthenticationHttpBasic: auth["scheme"] = t.Scheme auth["user"] = t.User auth["password"] = t.Password - case *cloudflare.AccessApplicationScimAuthenticationOauthBearerToken: auth["scheme"] = t.Scheme auth["token"] = t.Token @@ -1479,11 +1482,43 @@ func convertScimConfigAuthenticationStructToSchema(scimAuth *cloudflare.AccessAp auth["authorization_url"] = t.AuthorizationURL auth["token_url"] = t.TokenURL auth["scopes"] = t.Scopes + case *cloudflare.AccessApplicationScimAuthenticationServiceToken: + auth["scheme"] = t.Scheme + auth["client_id"] = t.ClientID + auth["client_secret"] = t.ClientSecret } return []interface{}{auth} } +func convertScimConfigSingleAuthentiationToSchema(scimAuth *cloudflare.AccessApplicationScimAuthenticationJson) interface{} { + auth := map[string]interface{}{} + + switch t := scimAuth.Value.(type) { + case *cloudflare.AccessApplicationScimAuthenticationHttpBasic: + auth["scheme"] = t.Scheme + auth["user"] = t.User + auth["password"] = t.Password + + case *cloudflare.AccessApplicationScimAuthenticationOauthBearerToken: + auth["scheme"] = t.Scheme + auth["token"] = t.Token + case *cloudflare.AccessApplicationScimAuthenticationOauth2: + auth["scheme"] = t.Scheme + auth["client_id"] = t.ClientID + auth["client_secret"] = t.ClientSecret + auth["authorization_url"] = t.AuthorizationURL + auth["token_url"] = t.TokenURL + auth["scopes"] = t.Scopes + case *cloudflare.AccessApplicationScimAuthenticationServiceToken: + auth["scheme"] = t.Scheme + auth["client_id"] = t.ClientID + auth["client_secret"] = t.ClientSecret + } + + return auth +} + func convertScimConfigMappingsStructsToSchema(mappingsData []*cloudflare.AccessApplicationScimMapping) []interface{} { mappings := []interface{}{} diff --git a/internal/sdkv2provider/schema_cloudflare_teams_location.go b/internal/sdkv2provider/schema_cloudflare_teams_location.go index 2d24e4cdf9..08334f0abf 100644 --- a/internal/sdkv2provider/schema_cloudflare_teams_location.go +++ b/internal/sdkv2provider/schema_cloudflare_teams_location.go @@ -45,11 +45,6 @@ func resourceCloudflareTeamsLocationSchema() map[string]*schema.Schema { Optional: true, Description: "Indicator that this location needs to resolve EDNS queries.", }, - "policy_ids": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Computed: true, - }, "ip": { Type: schema.TypeString, Computed: true,