Skip to content

Commit 875a58f

Browse files
committed
add write-only option for token attribute for notification configuration resource
1 parent 2130664 commit 875a58f

File tree

4 files changed

+153
-15
lines changed

4 files changed

+153
-15
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ ENHANCEMENTS:
2525

2626
* resource/tfe_saml_settings: Add `private_key_wo` write-only attribute, by @uturunku1 ([#1660](https://github.com/hashicorp/terraform-provider-tfe/pull/1660))
2727

28+
* resource/tfe_notification_configuration: Add `token_wo` write-only attribute, by @uturunku1 ([#1664](https://github.com/hashicorp/terraform-provider-tfe/pull/1664))
29+
2830
## v.0.64.0
2931

3032
FEATURES:

internal/provider/resource_tfe_notification_configuration.go

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2222
"github.com/hashicorp/terraform-plugin-framework/types"
2323
"github.com/hashicorp/terraform-plugin-log/tflog"
24+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/helpers"
25+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/planmodifiers"
2426
"github.com/hashicorp/terraform-provider-tfe/internal/provider/validators"
2527
)
2628

@@ -48,13 +50,14 @@ type modelTFENotificationConfiguration struct {
4850
EmailUserIDs types.Set `tfsdk:"email_user_ids"`
4951
Enabled types.Bool `tfsdk:"enabled"`
5052
Token types.String `tfsdk:"token"`
53+
TokenWO types.String `tfsdk:"token_wo"`
5154
Triggers types.Set `tfsdk:"triggers"`
5255
URL types.String `tfsdk:"url"`
5356
WorkspaceID types.String `tfsdk:"workspace_id"`
5457
}
5558

5659
// modelFromTFENotificationConfiguration builds a modelTFENotificationConfiguration struct from a tfe.NotificationConfiguration value.
57-
func modelFromTFENotificationConfiguration(v *tfe.NotificationConfiguration) (modelTFENotificationConfiguration, diag.Diagnostics) {
60+
func modelFromTFENotificationConfiguration(v *tfe.NotificationConfiguration, isWriteOnlyValue bool) (modelTFENotificationConfiguration, diag.Diagnostics) {
5861
var diags diag.Diagnostics
5962
result := modelTFENotificationConfiguration{
6063
ID: types.StringValue(v.ID),
@@ -99,6 +102,10 @@ func modelFromTFENotificationConfiguration(v *tfe.NotificationConfiguration) (mo
99102
if v.Token != "" {
100103
result.Token = types.StringValue(v.Token)
101104
}
105+
// Don't retrieve values if write-only is being used. Unset the value and readable_value fields before updating the state.
106+
if isWriteOnlyValue {
107+
result.Token = types.StringNull()
108+
}
102109

103110
if v.URL != "" {
104111
result.URL = types.StringValue(v.URL)
@@ -196,7 +203,6 @@ func (r *resourceTFENotificationConfiguration) Schema(ctx context.Context, req r
196203
Computed: true,
197204
Default: booldefault.StaticBool(false),
198205
},
199-
200206
"token": schema.StringAttribute{
201207
Description: "A write-only secure token for the notification configuration, which can be used by the receiving server to verify request authenticity when configured for notification configurations with a destination type of `generic`. Defaults to `null`. This value _must not_ be provided if `destination_type` is `email`, `microsoft-teams`, or `slack`.",
202208
Optional: true,
@@ -206,9 +212,21 @@ func (r *resourceTFENotificationConfiguration) Schema(ctx context.Context, req r
206212
"destination_type",
207213
[]string{"email", "microsoft-teams", "slack"},
208214
),
215+
stringvalidator.ConflictsWith(path.MatchRoot("token_wo")),
216+
},
217+
},
218+
"token_wo": schema.StringAttribute{
219+
Optional: true,
220+
WriteOnly: true,
221+
Sensitive: true,
222+
Description: "Value of the token in write-only mode",
223+
Validators: []validator.String{
224+
stringvalidator.ConflictsWith(path.MatchRoot("token")),
225+
},
226+
PlanModifiers: []planmodifier.String{
227+
planmodifiers.NewReplaceForWriteOnlyStringValue("token_wo"),
209228
},
210229
},
211-
212230
"triggers": schema.SetAttribute{
213231
Description: "The array of triggers for which this notification configuration will send notifications. If omitted, no notification triggers are configured.",
214232
Optional: true,
@@ -264,11 +282,15 @@ func (r *resourceTFENotificationConfiguration) Schema(ctx context.Context, req r
264282

265283
// Create implements resource.Resource
266284
func (r *resourceTFENotificationConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
267-
var plan modelTFENotificationConfiguration
268-
285+
var plan, config modelTFENotificationConfiguration
269286
// Read Terraform plan data into the model
270287
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
288+
if resp.Diagnostics.HasError() {
289+
return
290+
}
271291

292+
diags := req.Config.Get(ctx, &config)
293+
resp.Diagnostics.Append(diags...)
272294
if resp.Diagnostics.HasError() {
273295
return
274296
}
@@ -280,13 +302,19 @@ func (r *resourceTFENotificationConfiguration) Create(ctx context.Context, req r
280302
DestinationType: tfe.NotificationDestination(tfe.NotificationDestinationType(plan.DestinationType.ValueString())),
281303
Enabled: plan.Enabled.ValueBoolPointer(),
282304
Name: plan.Name.ValueStringPointer(),
283-
Token: plan.Token.ValueStringPointer(),
284305
URL: plan.URL.ValueStringPointer(),
285306
SubscribableChoice: &tfe.NotificationConfigurationSubscribableChoice{
286307
Workspace: &tfe.Workspace{ID: workspaceID},
287308
},
288309
}
289310

311+
// Set Value from `token_wo` if set, otherwise use the normal value
312+
if !config.TokenWO.IsNull() {
313+
options.Token = config.TokenWO.ValueStringPointer()
314+
} else {
315+
options.Token = plan.Token.ValueStringPointer()
316+
}
317+
290318
// Add triggers set to the options struct
291319
var triggers []types.String
292320
if diags := plan.Triggers.ElementsAs(ctx, &triggers, true); diags != nil && diags.HasError() {
@@ -336,12 +364,20 @@ func (r *resourceTFENotificationConfiguration) Create(ctx context.Context, req r
336364
nc.Token = plan.Token.ValueString()
337365
}
338366

339-
result, diags := modelFromTFENotificationConfiguration(nc)
367+
// We got a notification, so set state to new values
368+
result, diags := modelFromTFENotificationConfiguration(nc, !config.TokenWO.IsNull())
340369
if diags != nil && diags.HasError() {
341370
resp.Diagnostics.Append((diags)...)
342371
return
343372
}
344373

374+
// Store the hashed write-only value in the private state
375+
store := r.writeOnlyValueStore(resp.Private)
376+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.TokenWO)...)
377+
if resp.Diagnostics.HasError() {
378+
return
379+
}
380+
345381
// Save data into Terraform state
346382
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
347383
}
@@ -368,7 +404,13 @@ func (r *resourceTFENotificationConfiguration) Read(ctx context.Context, req res
368404
nc.Token = state.Token.ValueString()
369405
}
370406

371-
result, diags := modelFromTFENotificationConfiguration(nc)
407+
isWriteOnly, diags := r.writeOnlyValueStore(resp.Private).PriorValueExists(ctx)
408+
resp.Diagnostics.Append(diags...)
409+
if diags.HasError() {
410+
return
411+
}
412+
413+
result, diags := modelFromTFENotificationConfiguration(nc, isWriteOnly)
372414
if diags != nil && diags.HasError() {
373415
resp.Diagnostics.Append((diags)...)
374416
return
@@ -380,15 +422,19 @@ func (r *resourceTFENotificationConfiguration) Read(ctx context.Context, req res
380422

381423
// Update implements resource.Resource
382424
func (r *resourceTFENotificationConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
383-
var plan modelTFENotificationConfiguration
384-
var state modelTFENotificationConfiguration
385-
425+
var plan, state, config modelTFENotificationConfiguration
386426
// Read Terraform plan data into the model
387427
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
388-
428+
if resp.Diagnostics.HasError() {
429+
return
430+
}
389431
// Read Terraform prior state data into the model
390432
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
391-
433+
if resp.Diagnostics.HasError() {
434+
return
435+
}
436+
// Read configuration data into the model
437+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
392438
if resp.Diagnostics.HasError() {
393439
return
394440
}
@@ -449,7 +495,14 @@ func (r *resourceTFENotificationConfiguration) Update(ctx context.Context, req r
449495
nc.Token = plan.Token.ValueString()
450496
}
451497

452-
result, diags := modelFromTFENotificationConfiguration(nc)
498+
// Store the hashed write-only value in the private state
499+
store := r.writeOnlyValueStore(resp.Private)
500+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.TokenWO)...)
501+
if resp.Diagnostics.HasError() {
502+
return
503+
}
504+
505+
result, diags := modelFromTFENotificationConfiguration(nc, !config.TokenWO.IsNull())
453506
if diags != nil && diags.HasError() {
454507
resp.Diagnostics.Append((diags)...)
455508
return
@@ -480,3 +533,7 @@ func (r *resourceTFENotificationConfiguration) Delete(ctx context.Context, req r
480533
func (r *resourceTFENotificationConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
481534
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
482535
}
536+
537+
func (r *resourceTFENotificationConfiguration) writeOnlyValueStore(private helpers.PrivateState) *helpers.WriteOnlyValueStore {
538+
return helpers.NewWriteOnlyValueStore(private, "token_wo")
539+
}

internal/provider/resource_tfe_notification_configuration_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,40 @@ func TestAccTFENotificationConfiguration_basic(t *testing.T) {
4747
})
4848
}
4949

50+
func TestAccTFENotificationConfiguration_WriteOnly(t *testing.T) {
51+
notificationConfiguration := &tfe.NotificationConfiguration{}
52+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
53+
54+
resource.Test(t, resource.TestCase{
55+
PreCheck: func() { preCheckTFENotificationConfiguration(t) },
56+
ProtoV5ProviderFactories: testAccMuxedProviders,
57+
CheckDestroy: testAccCheckTFENotificationConfigurationDestroy,
58+
Steps: []resource.TestStep{
59+
{
60+
Config: testAccTFENotificationConfiguration_tokenAndTokenWriteOnly(rInt),
61+
ExpectError: regexp.MustCompile(`Attribute "token_wo" cannot be specified when "token" is specified`),
62+
},
63+
{
64+
Config: testAccTFENotificationConfiguration_tokenWriteOnly(rInt),
65+
Check: resource.ComposeTestCheckFunc(
66+
testAccCheckTFENotificationConfigurationExists(
67+
"tfe_notification_configuration.foobar", notificationConfiguration),
68+
testAccCheckTFENotificationConfigurationAttributes(notificationConfiguration),
69+
resource.TestCheckResourceAttr(
70+
"tfe_notification_configuration.foobar", "destination_type", "generic"),
71+
resource.TestCheckResourceAttr(
72+
"tfe_notification_configuration.foobar", "name", "notification_basic"),
73+
resource.TestCheckResourceAttr(
74+
"tfe_notification_configuration.foobar", "triggers.#", "0"),
75+
resource.TestCheckResourceAttr(
76+
"tfe_notification_configuration.foobar", "url", runTasksURL()),
77+
resource.TestCheckNoResourceAttr("tfe_notification_configuration.foobar", "token_wo"),
78+
),
79+
},
80+
},
81+
})
82+
}
83+
5084
func TestAccTFENotificationConfiguration_emailUserIDs(t *testing.T) {
5185
notificationConfiguration := &tfe.NotificationConfiguration{}
5286
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
@@ -816,6 +850,49 @@ resource "tfe_notification_configuration" "foobar" {
816850
}`, rInt, runTasksURL())
817851
}
818852

853+
func testAccTFENotificationConfiguration_tokenWriteOnly(rInt int) string {
854+
return fmt.Sprintf(`
855+
resource "tfe_organization" "foobar" {
856+
name = "tst-terraform-%d"
857+
858+
}
859+
860+
resource "tfe_workspace" "foobar" {
861+
name = "workspace-test"
862+
organization = tfe_organization.foobar.id
863+
}
864+
865+
resource "tfe_notification_configuration" "foobar" {
866+
name = "notification_basic"
867+
destination_type = "generic"
868+
token = "some-token"
869+
url = "%s"
870+
workspace_id = tfe_workspace.foobar.id
871+
}`, rInt, runTasksURL())
872+
}
873+
874+
func testAccTFENotificationConfiguration_tokenAndTokenWriteOnly(rInt int) string {
875+
return fmt.Sprintf(`
876+
resource "tfe_organization" "foobar" {
877+
name = "tst-terraform-%d"
878+
879+
}
880+
881+
resource "tfe_workspace" "foobar" {
882+
name = "workspace-test"
883+
organization = tfe_organization.foobar.id
884+
}
885+
886+
resource "tfe_notification_configuration" "foobar" {
887+
name = "notification_basic"
888+
destination_type = "generic"
889+
token = "some-token"
890+
token_wo = "some-token"
891+
url = "%s"
892+
workspace_id = tfe_workspace.foobar.id
893+
}`, rInt, runTasksURL())
894+
}
895+
819896
func testAccTFENotificationConfiguration_emailUserIDs(rInt int) string {
820897
return fmt.Sprintf(`
821898
resource "tfe_organization" "foobar" {

website/docs/r/notification_configuration.html.markdown

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ The following arguments are supported:
111111
if `destination_type` is `generic`, `microsoft-teams`, or `slack`.
112112
* `enabled` - (Optional) Whether the notification configuration should be enabled or not.
113113
Disabled configurations will not send any notifications. Defaults to `false`.
114-
* `token` - (Optional) A write-only secure token for the notification configuration, which can
114+
* `token` - (Optional) A token for the notification configuration, which can
115115
be used by the receiving server to verify request authenticity when configured for notification
116116
configurations with a destination type of `generic`. Defaults to `null`.
117117
This value _must not_ be provided if `destination_type` is `email`, `microsoft-teams`, or `slack`.
@@ -124,6 +124,8 @@ The following arguments are supported:
124124
is `email`.
125125
* `workspace_id` - (Required) The id of the workspace that owns the notification configuration.
126126

127+
-> **Note:** Write-Only argument `token_wo` is available to use in place of `token`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/v1.11.x/resources/ephemeral#write-only-arguments).
128+
127129
## Attributes Reference
128130

129131
* `id` - The ID of the notification configuration.

0 commit comments

Comments
 (0)