From f88a8d56a2345c49a492e4f6980ee2b80099bd92 Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 8 Jul 2026 12:24:06 +0100 Subject: [PATCH] feat(workers): add write-only secret fields for bindings and access idp Add write-only `_wo` fields for Access IdP client secrets and Workers binding text values to keep secrets out of Terraform state while preserving backward compatibility with existing sensitive attributes. --- docs/resources/worker_version.md | 2 ++ docs/resources/workers_script.md | 2 ++ .../zero_trust_access_identity_provider.md | 2 ++ internal/services/worker_version/model.go | 2 ++ internal/services/worker_version/resource.go | 26 ++++++++++++++++ internal/services/worker_version/schema.go | 16 ++++++++++ internal/services/workers_script/model.go | 4 ++- internal/services/workers_script/resource.go | 28 +++++++++++++++++ internal/services/workers_script/schema.go | 30 ++++++++++++++----- .../model.go | 2 ++ .../resource.go | 14 +++++++++ .../schema.go | 16 ++++++++++ 12 files changed, 136 insertions(+), 8 deletions(-) diff --git a/docs/resources/worker_version.md b/docs/resources/worker_version.md index d8430e3712..b6383e7067 100644 --- a/docs/resources/worker_version.md +++ b/docs/resources/worker_version.md @@ -206,6 +206,8 @@ Available values: "eu", "fedramp", "fedramp-high". - `simple` (Attributes) The rate limit configuration. (see [below for nested schema](#nestedatt--bindings--simple)) - `store_id` (String) ID of the store containing the secret. - `text` (String, Sensitive) The text value to use. +- `text_wo` (String, Sensitive, Write-Only) Write-only text value to use. Requires Terraform 1.11+. +- `text_wo_version` (Number) Version trigger for `text_wo` updates. - `tunnel_id` (String) UUID of the Cloudflare Tunnel to bind to. Mutually exclusive with network_id. - `usages` (Set of String) Allowed operations with the key. [Learn more](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyUsages). - `version_id` (String) Identifier for the version to inherit the binding from, which can be the version ID or the literal "latest" to inherit from the latest version. Defaults to inheriting the binding from the latest version. diff --git a/docs/resources/workers_script.md b/docs/resources/workers_script.md index 12f74329e2..8a5893a177 100644 --- a/docs/resources/workers_script.md +++ b/docs/resources/workers_script.md @@ -245,6 +245,8 @@ Available values: "eu", "fedramp", "fedramp-high". - `simple` (Attributes) A simple rate limit. (see [below for nested schema](#nestedatt--bindings--simple)) - `store_id` (String) ID of the store containing the secret. - `text` (String, Sensitive) The text value to use. +- `text_wo` (String, Sensitive, Write-Only) Write-only text value to use. Requires Terraform 1.11+. +- `text_wo_version` (Number) Version trigger for `text_wo` updates. - `tunnel_id` (String) UUID of the Cloudflare Tunnel to bind to. Mutually exclusive with network_id. - `usages` (Set of String) Allowed operations with the key. [Learn more](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyUsages). - `version_id` (String) Identifier for the version to inherit the binding from, which can be the version ID or the literal "latest" to inherit from the latest version. Defaults to inheriting the binding from the latest version. diff --git a/docs/resources/zero_trust_access_identity_provider.md b/docs/resources/zero_trust_access_identity_provider.md index dbf968c049..3928a9c2ab 100644 --- a/docs/resources/zero_trust_access_identity_provider.md +++ b/docs/resources/zero_trust_access_identity_provider.md @@ -80,6 +80,8 @@ Optional: - `claims` (List of String) Custom claims - `client_id` (String) Your OAuth Client ID - `client_secret` (String, Sensitive) Your OAuth Client Secret +- `client_secret_wo` (String, Sensitive, Write-Only) Write-only OAuth Client Secret. Requires Terraform 1.11+. +- `client_secret_wo_version` (Number) Version trigger for `client_secret_wo` updates. - `conditional_access_enabled` (Boolean) Should Cloudflare try to load authentication contexts from your account - `directory_id` (String) Your Azure directory uuid - `email_attribute_name` (String) The attribute name for email in the SAML response. diff --git a/internal/services/worker_version/model.go b/internal/services/worker_version/model.go index cbca6fc7b1..7f2d5acf59 100644 --- a/internal/services/worker_version/model.go +++ b/internal/services/worker_version/model.go @@ -153,6 +153,8 @@ type WorkerVersionBindingsModel struct { Json jsontypes.Normalized `tfsdk:"json" json:"json,optional"` CertificateID types.String `tfsdk:"certificate_id" json:"certificate_id,optional"` Text types.String `tfsdk:"text" json:"text,optional"` + TextWO types.String `tfsdk:"text_wo" json:"-,optional"` + TextWOVersion types.Int64 `tfsdk:"text_wo_version" json:"-,optional"` Pipeline types.String `tfsdk:"pipeline" json:"pipeline,optional"` QueueName types.String `tfsdk:"queue_name" json:"queue_name,optional"` Simple *WorkerVersionBindingsSimpleModel `tfsdk:"simple" json:"simple,optional"` diff --git a/internal/services/worker_version/resource.go b/internal/services/worker_version/resource.go index 40d87b3713..4477e54ade 100644 --- a/internal/services/worker_version/resource.go +++ b/internal/services/worker_version/resource.go @@ -122,6 +122,8 @@ func (r *WorkerVersionResource) Create(ctx context.Context, req resource.CreateR return } + applyWriteOnlyBindingText(data) + dataBytes, err := data.MarshalJSON() if err != nil { resp.Diagnostics.AddError("failed to serialize http request", err.Error()) @@ -419,3 +421,27 @@ func (r *WorkerVersionResource) ImportState(ctx context.Context, req resource.Im func (r *WorkerVersionResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, _ *resource.ModifyPlanResponse) { } + +func applyWriteOnlyBindingText(data *WorkerVersionModel) { + if data == nil || data.Bindings.IsNull() || data.Bindings.IsUnknown() { + return + } + + var bindings []WorkerVersionBindingsModel + diags := data.Bindings.ElementsAs(context.Background(), &bindings, true) + if diags.HasError() { + return + } + + for i := range bindings { + if !bindings[i].TextWO.IsNull() && !bindings[i].TextWO.IsUnknown() { + bindings[i].Text = bindings[i].TextWO + } + } + + updated, diags := customfield.NewObjectList(context.Background(), bindings) + if diags.HasError() { + return + } + data.Bindings = updated +} diff --git a/internal/services/worker_version/schema.go b/internal/services/worker_version/schema.go index 061fc1aedb..73755032a7 100644 --- a/internal/services/worker_version/schema.go +++ b/internal/services/worker_version/schema.go @@ -555,6 +555,22 @@ func ResourceSchema(ctx context.Context) schema.Schema { Description: "The text value to use.", Optional: true, Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("text_wo")), + }, + }, + "text_wo": schema.StringAttribute{ + Description: "Write-only text value to use. Requires Terraform 1.11+.", + Optional: true, + Sensitive: true, + WriteOnly: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("text")), + }, + }, + "text_wo_version": schema.Int64Attribute{ + Description: "Version trigger for text_wo updates.", + Optional: true, }, "pipeline": schema.StringAttribute{ Description: "Name of the Pipeline to bind to.", diff --git a/internal/services/workers_script/model.go b/internal/services/workers_script/model.go index 58d5e9c08a..0516c4a29e 100644 --- a/internal/services/workers_script/model.go +++ b/internal/services/workers_script/model.go @@ -98,7 +98,7 @@ func (r WorkersScriptModel) MarshalMultipart() (data []byte, formDataContentType } type WorkersScriptMetadataModel struct { - Annotations customfield.NestedObject[WorkersScriptMetadataAnnotationsModel] `tfsdk:"annotations" json:"annotations,computed_optional"` + Annotations customfield.NestedObject[WorkersScriptMetadataAnnotationsModel] `tfsdk:"annotations" json:"annotations,computed_optional"` Assets *WorkersScriptMetadataAssetsModel `tfsdk:"assets" json:"assets,optional"` Bindings customfield.NestedObjectList[WorkersScriptMetadataBindingsModel] `tfsdk:"bindings" json:"bindings,computed_optional"` BodyPart types.String `tfsdk:"body_part" json:"body_part,optional"` @@ -154,6 +154,8 @@ type WorkersScriptMetadataBindingsModel struct { Json jsontypes.Normalized `tfsdk:"json" json:"json,optional"` CertificateID types.String `tfsdk:"certificate_id" json:"certificate_id,optional"` Text types.String `tfsdk:"text" json:"text,optional"` + TextWO types.String `tfsdk:"text_wo" json:"-,optional"` + TextWOVersion types.Int64 `tfsdk:"text_wo_version" json:"-,optional"` Pipeline types.String `tfsdk:"pipeline" json:"pipeline,optional"` QueueName types.String `tfsdk:"queue_name" json:"queue_name,optional"` Simple *WorkersScriptMetadataBindingsSimpleModel `tfsdk:"simple" json:"simple,optional"` diff --git a/internal/services/workers_script/resource.go b/internal/services/workers_script/resource.go index 1630fd95c6..d1ee3d1261 100644 --- a/internal/services/workers_script/resource.go +++ b/internal/services/workers_script/resource.go @@ -107,6 +107,8 @@ func (r *WorkersScriptResource) Create(ctx context.Context, req resource.CreateR data.Content = types.StringValue(content) } + applyWriteOnlyBindingText(data) + dataBytes, formDataContentType, err := data.MarshalMultipart() if err != nil { resp.Diagnostics.AddError("failed to serialize multipart http request", err.Error()) @@ -201,6 +203,8 @@ func (r *WorkersScriptResource) Update(ctx context.Context, req resource.UpdateR data.Content = types.StringValue(content) } + applyWriteOnlyBindingText(data) + dataBytes, formDataContentType, err := data.MarshalMultipart() if err != nil { resp.Diagnostics.AddError("failed to serialize multipart http request", err.Error()) @@ -487,3 +491,27 @@ func (r *WorkersScriptResource) ModifyPlan(ctx context.Context, req resource.Mod resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("modified_on"), timetypes.NewRFC3339Unknown())...) resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("has_assets"), types.BoolUnknown())...) } + +func applyWriteOnlyBindingText(data *WorkersScriptModel) { + if data == nil || data.Bindings.IsNull() || data.Bindings.IsUnknown() { + return + } + + var bindings []WorkersScriptMetadataBindingsModel + diags := data.Bindings.ElementsAs(context.Background(), &bindings, true) + if diags.HasError() { + return + } + + for i := range bindings { + if !bindings[i].TextWO.IsNull() && !bindings[i].TextWO.IsUnknown() { + bindings[i].Text = bindings[i].TextWO + } + } + + updated, diags := customfield.NewObjectList(context.Background(), bindings) + if diags.HasError() { + return + } + data.Bindings = updated +} diff --git a/internal/services/workers_script/schema.go b/internal/services/workers_script/schema.go index 36e837b2ae..74f42381df 100644 --- a/internal/services/workers_script/schema.go +++ b/internal/services/workers_script/schema.go @@ -220,10 +220,10 @@ func ResourceSchema(ctx context.Context) schema.Schema { ), }, }, - "dataset": schema.StringAttribute{ - Description: "The name of the dataset to bind to.", - Optional: true, - }, + "dataset": schema.StringAttribute{ + Description: "The name of the dataset to bind to.", + Optional: true, + }, "id": schema.StringAttribute{ Description: "Identifier of the D1 database to bind to.", Optional: true, @@ -294,6 +294,22 @@ func ResourceSchema(ctx context.Context) schema.Schema { Description: "The text value to use.", Optional: true, Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("text_wo")), + }, + }, + "text_wo": schema.StringAttribute{ + Description: "Write-only text value to use. Requires Terraform 1.11+.", + Optional: true, + Sensitive: true, + WriteOnly: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("text")), + }, + }, + "text_wo_version": schema.Int64Attribute{ + Description: "Version trigger for text_wo updates.", + Optional: true, }, "pipeline": schema.StringAttribute{ Description: "Name of the Pipeline to bind to.", @@ -695,9 +711,9 @@ func ResourceSchema(ctx context.Context) schema.Schema { Default: booldefault.StaticBool(true), }, "propagation_policy": schema.StringAttribute{ - Description: "Controls how inbound trace context (traceparent/tracestate) headers on incoming requests are handled. \"authenticated\" (default) honors inbound trace context only when accompanied by a valid trace auth token. \"accept\" unconditionally accepts inbound trace context. Requires the trace propagation feature to be enabled.\nAvailable values: \"authenticated\", \"accept\".", - Computed: true, - Optional: true, + Description: "Controls how inbound trace context (traceparent/tracestate) headers on incoming requests are handled. \"authenticated\" (default) honors inbound trace context only when accompanied by a valid trace auth token. \"accept\" unconditionally accepts inbound trace context. Requires the trace propagation feature to be enabled.\nAvailable values: \"authenticated\", \"accept\".", + Computed: true, + Optional: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, }, diff --git a/internal/services/zero_trust_access_identity_provider/model.go b/internal/services/zero_trust_access_identity_provider/model.go index 340c709ff4..8faaf0fb90 100644 --- a/internal/services/zero_trust_access_identity_provider/model.go +++ b/internal/services/zero_trust_access_identity_provider/model.go @@ -39,6 +39,8 @@ type ZeroTrustAccessIdentityProviderConfigModel struct { Claims *[]types.String `tfsdk:"claims" json:"claims,optional"` ClientID types.String `tfsdk:"client_id" json:"client_id,optional"` ClientSecret types.String `tfsdk:"client_secret" json:"client_secret,optional"` + ClientSecretWO types.String `tfsdk:"client_secret_wo" json:"-,optional"` + ClientSecretWOVersion types.Int64 `tfsdk:"client_secret_wo_version" json:"-,optional"` ConditionalAccessEnabled types.Bool `tfsdk:"conditional_access_enabled" json:"conditional_access_enabled,optional"` DirectoryID types.String `tfsdk:"directory_id" json:"directory_id,optional"` EmailClaimName types.String `tfsdk:"email_claim_name" json:"email_claim_name,optional"` diff --git a/internal/services/zero_trust_access_identity_provider/resource.go b/internal/services/zero_trust_access_identity_provider/resource.go index 683fb22798..fe22841e99 100644 --- a/internal/services/zero_trust_access_identity_provider/resource.go +++ b/internal/services/zero_trust_access_identity_provider/resource.go @@ -65,6 +65,8 @@ func (r *ZeroTrustAccessIdentityProviderResource) Create(ctx context.Context, re return } + applyWriteOnlyClientSecret(data) + dataBytes, err := data.MarshalJSON() if err != nil { resp.Diagnostics.AddError("failed to serialize http request", err.Error()) @@ -119,6 +121,8 @@ func (r *ZeroTrustAccessIdentityProviderResource) Update(ctx context.Context, re return } + applyWriteOnlyClientSecret(data) + dataBytes, err := data.MarshalJSONForUpdate(*state) if err != nil { resp.Diagnostics.AddError("failed to serialize http request", err.Error()) @@ -313,3 +317,13 @@ func (r *ZeroTrustAccessIdentityProviderResource) ImportState(ctx context.Contex func (r *ZeroTrustAccessIdentityProviderResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res *resource.ModifyPlanResponse) { modifyPlan(ctx, req, res) } + +func applyWriteOnlyClientSecret(data *ZeroTrustAccessIdentityProviderModel) { + if data == nil || data.Config == nil { + return + } + + if !data.Config.ClientSecretWO.IsNull() && !data.Config.ClientSecretWO.IsUnknown() { + data.Config.ClientSecret = data.Config.ClientSecretWO + } +} diff --git a/internal/services/zero_trust_access_identity_provider/schema.go b/internal/services/zero_trust_access_identity_provider/schema.go index 0a4b8d6dc8..90f7b29a04 100644 --- a/internal/services/zero_trust_access_identity_provider/schema.go +++ b/internal/services/zero_trust_access_identity_provider/schema.go @@ -97,6 +97,22 @@ func ResourceSchema(ctx context.Context) schema.Schema { Description: "Your OAuth Client Secret", Optional: true, Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("client_secret_wo")), + }, + }, + "client_secret_wo": schema.StringAttribute{ + Description: "Write-only OAuth Client Secret. Requires Terraform 1.11+.", + Optional: true, + Sensitive: true, + WriteOnly: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("client_secret")), + }, + }, + "client_secret_wo_version": schema.Int64Attribute{ + Description: "Version trigger for client_secret_wo updates.", + Optional: true, }, "conditional_access_enabled": schema.BoolAttribute{ Description: "Should Cloudflare try to load authentication contexts from your account",