Skip to content

Commit 175f48d

Browse files
authored
Merge pull request #1660 from hashicorp/token-wo
feat: add write-only private_key_wo attribute to tfe_saml_settings
2 parents 586f314 + 143a67e commit 175f48d

File tree

5 files changed

+162
-8
lines changed

5 files changed

+162
-8
lines changed

CHANGELOG.md

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

2424
* resource/tfe_organization_run_task: Add `hmac_key_wo` write-only attribute, by @shwetamurali ([#1646](https://github.com/hashicorp/terraform-provider-tfe/pull/1646))
2525

26+
* resource/tfe_saml_settings: Add `private_key_wo` write-only attribute, by @uturunku1 ([#1660](https://github.com/hashicorp/terraform-provider-tfe/pull/1660))
27+
2628
## v.0.64.0
2729

2830
FEATURES:

internal/provider/data_source_saml_settings.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ type dataSourceTFESAMLSettings struct {
2929
client *tfe.Client
3030
}
3131

32-
// modelTFESAMLSettings maps the data source schema data.
33-
type modelTFESAMLSettings struct {
32+
// modelDataTFESAMLSettings maps the data source schema data.
33+
type modelDataTFESAMLSettings struct {
3434
ID types.String `tfsdk:"id"`
3535
Enabled types.Bool `tfsdk:"enabled"`
3636
Debug types.Bool `tfsdk:"debug"`
@@ -158,7 +158,7 @@ func (d *dataSourceTFESAMLSettings) Read(ctx context.Context, _ datasource.ReadR
158158
}
159159

160160
// Set state
161-
diags := resp.State.Set(ctx, &modelTFESAMLSettings{
161+
diags := resp.State.Set(ctx, &modelDataTFESAMLSettings{
162162
ID: types.StringValue(s.ID),
163163
Enabled: types.BoolValue(s.Enabled),
164164
Debug: types.BoolValue(s.Debug),

internal/provider/resource_tfe_saml_settings.go

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import (
99

1010
tfe "github.com/hashicorp/go-tfe"
1111
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
1213
"github.com/hashicorp/terraform-plugin-framework/resource"
1314
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1415
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1516
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1618
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
1719
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1820
"github.com/hashicorp/terraform-plugin-framework/types"
1921
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
2022
"github.com/hashicorp/terraform-plugin-log/tflog"
23+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/helpers"
24+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/planmodifiers"
2125
)
2226

2327
const (
@@ -30,13 +34,38 @@ const (
3034
samlDefaultSSOAPITokenSessionTimeoutSeconds int64 = 1209600 // 14 days
3135
)
3236

37+
type modelTFESAMLSettings struct {
38+
ID types.String `tfsdk:"id"`
39+
Enabled types.Bool `tfsdk:"enabled"`
40+
Debug types.Bool `tfsdk:"debug"`
41+
TeamManagementEnabled types.Bool `tfsdk:"team_management_enabled"`
42+
AuthnRequestsSigned types.Bool `tfsdk:"authn_requests_signed"`
43+
WantAssertionsSigned types.Bool `tfsdk:"want_assertions_signed"`
44+
IDPCert types.String `tfsdk:"idp_cert"`
45+
OldIDPCert types.String `tfsdk:"old_idp_cert"`
46+
SLOEndpointURL types.String `tfsdk:"slo_endpoint_url"`
47+
SSOEndpointURL types.String `tfsdk:"sso_endpoint_url"`
48+
AttrUsername types.String `tfsdk:"attr_username"`
49+
AttrGroups types.String `tfsdk:"attr_groups"`
50+
AttrSiteAdmin types.String `tfsdk:"attr_site_admin"`
51+
SiteAdminRole types.String `tfsdk:"site_admin_role"`
52+
SSOAPITokenSessionTimeout types.Int64 `tfsdk:"sso_api_token_session_timeout"`
53+
ACSConsumerURL types.String `tfsdk:"acs_consumer_url"`
54+
MetadataURL types.String `tfsdk:"metadata_url"`
55+
Certificate types.String `tfsdk:"certificate"`
56+
PrivateKey types.String `tfsdk:"private_key"`
57+
PrivateKeyWO types.String `tfsdk:"private_key_wo"`
58+
SignatureSigningMethod types.String `tfsdk:"signature_signing_method"`
59+
SignatureDigestMethod types.String `tfsdk:"signature_digest_method"`
60+
}
61+
3362
// resourceTFESAMLSettings implements the tfe_saml_settings resource type
3463
type resourceTFESAMLSettings struct {
3564
client *tfe.Client
3665
}
3766

3867
// modelFromTFEAdminSAMLSettings builds a modelTFESAMLSettings struct from a tfe.AdminSAMLSetting value
39-
func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.String) modelTFESAMLSettings {
68+
func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.String, isWriteOnly bool) modelTFESAMLSettings {
4069
m := modelTFESAMLSettings{
4170
ID: types.StringValue(v.ID),
4271
Enabled: types.BoolValue(v.Enabled),
@@ -60,9 +89,16 @@ func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.Stri
6089
SignatureSigningMethod: types.StringValue(v.SignatureSigningMethod),
6190
SignatureDigestMethod: types.StringValue(v.SignatureDigestMethod),
6291
}
92+
6393
if len(privateKey.String()) > 0 {
6494
m.PrivateKey = privateKey
6595
}
96+
97+
// Don't retrieve values if write-only is being used. Unset the private key field before updating the state.
98+
if isWriteOnly {
99+
m.PrivateKey = types.StringValue("")
100+
}
101+
66102
return m
67103
}
68104

@@ -187,6 +223,21 @@ func (r *resourceTFESAMLSettings) Schema(ctx context.Context, req resource.Schem
187223
Optional: true,
188224
Computed: true,
189225
Sensitive: true,
226+
Validators: []validator.String{
227+
stringvalidator.ConflictsWith(path.MatchRoot("private_key_wo")),
228+
},
229+
},
230+
"private_key_wo": schema.StringAttribute{
231+
Description: "The private key in write-only mode used for request and assertion signing",
232+
Optional: true,
233+
Sensitive: true,
234+
WriteOnly: true,
235+
Validators: []validator.String{
236+
stringvalidator.ConflictsWith(path.MatchRoot("private_key")),
237+
},
238+
PlanModifiers: []planmodifier.String{
239+
planmodifiers.NewReplaceForWriteOnlyStringValue("private_key_wo"),
240+
},
190241
},
191242
"signature_signing_method": schema.StringAttribute{
192243
Description: fmt.Sprintf("Signature Signing Method. Must be either `%s` or `%s`. Defaults to `%s`", samlSignatureMethodSHA1, samlSignatureMethodSHA256, samlSignatureMethodSHA256),
@@ -225,13 +276,22 @@ func (r *resourceTFESAMLSettings) Read(ctx context.Context, req resource.ReadReq
225276
return
226277
}
227278

279+
tflog.Debug(ctx, "Reading SAML Settings")
280+
228281
samlSettings, err := r.client.Admin.Settings.SAML.Read(ctx)
229282
if err != nil {
230283
resp.Diagnostics.AddError("Error reading SAML Settings", "Could not read SAML Settings, unexpected error: "+err.Error())
231284
return
232285
}
233286

234-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
287+
isWriteOnly, diags := r.writeOnlyValueStore(resp.Private).PriorValueExists(ctx)
288+
resp.Diagnostics.Append(diags...)
289+
if diags.HasError() {
290+
return
291+
}
292+
293+
// update state
294+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, isWriteOnly)
235295
diags = resp.State.Set(ctx, &result)
236296
resp.Diagnostics.Append(diags...)
237297
}
@@ -245,14 +305,28 @@ func (r *resourceTFESAMLSettings) Create(ctx context.Context, req resource.Creat
245305
return
246306
}
247307

308+
var config modelTFESAMLSettings
309+
diags = req.Config.Get(ctx, &config)
310+
resp.Diagnostics.Append(diags...)
311+
if resp.Diagnostics.HasError() {
312+
return
313+
}
314+
315+
if !config.PrivateKeyWO.IsNull() {
316+
m.PrivateKey = config.PrivateKeyWO
317+
}
318+
248319
tflog.Debug(ctx, "Create SAML Settings")
249320
samlSettings, err := r.updateSAMLSettings(ctx, m)
250321
if err != nil {
251322
resp.Diagnostics.AddError("Error creating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error())
252323
return
253324
}
254325

255-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
326+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, !config.PrivateKeyWO.IsNull())
327+
// Store the hashed write-only value in the private state
328+
store := r.writeOnlyValueStore(resp.Private)
329+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.PrivateKeyWO)...)
256330
diags = resp.State.Set(ctx, &result)
257331
resp.Diagnostics.Append(diags...)
258332
}
@@ -266,14 +340,33 @@ func (r *resourceTFESAMLSettings) Update(ctx context.Context, req resource.Updat
266340
return
267341
}
268342

343+
var config modelTFESAMLSettings
344+
diags = req.Config.Get(ctx, &config)
345+
resp.Diagnostics.Append(diags...)
346+
if resp.Diagnostics.HasError() {
347+
return
348+
}
349+
350+
if !config.PrivateKeyWO.IsNull() {
351+
m.PrivateKey = config.PrivateKeyWO
352+
}
353+
269354
tflog.Debug(ctx, "Update SAML Settings")
270355
samlSettings, err := r.updateSAMLSettings(ctx, m)
271356
if err != nil {
272357
resp.Diagnostics.AddError("Error updating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error())
273358
return
274359
}
275360

276-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
361+
// Store the hashed write-only value in the private state
362+
store := r.writeOnlyValueStore(resp.Private)
363+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.PrivateKeyWO)...)
364+
if resp.Diagnostics.HasError() {
365+
return
366+
}
367+
368+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, !config.PrivateKeyWO.IsNull())
369+
// Save data into Terraform state
277370
diags = resp.State.Set(ctx, &result)
278371
resp.Diagnostics.Append(diags...)
279372
}
@@ -321,7 +414,7 @@ func (r *resourceTFESAMLSettings) ImportState(ctx context.Context, req resource.
321414
return
322415
}
323416

324-
result := modelFromTFEAdminSAMLSettings(*samlSettings, types.StringValue(""))
417+
result := modelFromTFEAdminSAMLSettings(*samlSettings, types.StringValue(""), false)
325418
diags := resp.State.Set(ctx, &result)
326419
resp.Diagnostics.Append(diags...)
327420
}
@@ -363,3 +456,7 @@ func (r *resourceTFESAMLSettings) updateSAMLSettings(ctx context.Context, m mode
363456
}
364457
return s, nil
365458
}
459+
460+
func (r *resourceTFESAMLSettings) writeOnlyValueStore(private helpers.PrivateState) *helpers.WriteOnlyValueStore {
461+
return helpers.NewWriteOnlyValueStore(private, "private_key_wo")
462+
}

internal/provider/resource_tfe_saml_settings_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
"testing"
1111

1212
"github.com/hashicorp/go-tfe"
13+
"github.com/hashicorp/go-version"
1314
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1415
"github.com/hashicorp/terraform-plugin-testing/terraform"
16+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
1517
)
1618

1719
const testResourceName = "tfe_saml_settings.foobar"
@@ -30,6 +32,46 @@ const testResourceName = "tfe_saml_settings.foobar"
3032

3133
// TestAccTFESAMLSettings_omnibus test suite is skipped in the CI, and will only run in TFE Nightly workflow
3234
// Should this test name ever change, you will also need to update the regex in ci.yml
35+
func TestAccTFESAMLSettings_writeOnly(t *testing.T) {
36+
s := tfe.AdminSAMLSetting{
37+
IDPCert: "testIDPCertBasic",
38+
SLOEndpointURL: "https://foobar.com/slo_endpoint_url",
39+
SSOEndpointURL: "https://foobar.com/sso_endpoint_url",
40+
PrivateKey: "TestPrivateKeyFull",
41+
}
42+
resource.Test(t, resource.TestCase{
43+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
44+
tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))),
45+
},
46+
ProtoV5ProviderFactories: testAccMuxedProviders,
47+
Steps: []resource.TestStep{
48+
{
49+
Config: testAccTFESAMLSettings_writeOnly(s),
50+
Check: resource.ComposeTestCheckFunc(
51+
resource.TestCheckResourceAttr(testResourceName, "enabled", "true"),
52+
resource.TestCheckResourceAttr(testResourceName, "debug", "false"),
53+
resource.TestCheckResourceAttr(testResourceName, "authn_requests_signed", "false"),
54+
resource.TestCheckResourceAttr(testResourceName, "want_assertions_signed", "false"),
55+
resource.TestCheckResourceAttr(testResourceName, "team_management_enabled", "false"),
56+
resource.TestCheckResourceAttr(testResourceName, "idp_cert", s.IDPCert),
57+
resource.TestCheckResourceAttr(testResourceName, "slo_endpoint_url", s.SLOEndpointURL),
58+
resource.TestCheckResourceAttr(testResourceName, "sso_endpoint_url", s.SSOEndpointURL),
59+
resource.TestCheckResourceAttr(testResourceName, "attr_username", samlDefaultAttrUsername),
60+
resource.TestCheckResourceAttr(testResourceName, "attr_site_admin", samlDefaultAttrSiteAdmin),
61+
resource.TestCheckResourceAttr(testResourceName, "attr_groups", samlDefaultAttrGroups),
62+
resource.TestCheckResourceAttr(testResourceName, "site_admin_role", samlDefaultSiteAdminRole),
63+
resource.TestCheckResourceAttr(testResourceName, "sso_api_token_session_timeout", strconv.Itoa(int(samlDefaultSSOAPITokenSessionTimeoutSeconds))),
64+
resource.TestCheckResourceAttrSet(testResourceName, "acs_consumer_url"),
65+
resource.TestCheckResourceAttrSet(testResourceName, "metadata_url"),
66+
resource.TestCheckResourceAttr(testResourceName, "signature_signing_method", samlSignatureMethodSHA256),
67+
resource.TestCheckResourceAttr(testResourceName, "signature_digest_method", samlSignatureMethodSHA256),
68+
resource.TestCheckNoResourceAttr(
69+
testResourceName, "private_key_wo"),
70+
),
71+
},
72+
},
73+
})
74+
}
3375
func TestAccTFESAMLSettings_omnibus(t *testing.T) {
3476
t.Run("basic SAML settings resource", func(t *testing.T) {
3577
s := tfe.AdminSAMLSetting{
@@ -330,3 +372,13 @@ resource "tfe_saml_settings" "foobar" {
330372
signature_digest_method = "%s"
331373
}`, s.IDPCert, s.SLOEndpointURL, s.SSOEndpointURL, s.Debug, s.AuthnRequestsSigned, s.WantAssertionsSigned, s.TeamManagementEnabled, s.AttrUsername, s.AttrSiteAdmin, s.AttrGroups, s.SiteAdminRole, s.SSOAPITokenSessionTimeout, s.Certificate, s.PrivateKey, s.SignatureSigningMethod, s.SignatureDigestMethod)
332374
}
375+
376+
func testAccTFESAMLSettings_writeOnly(s tfe.AdminSAMLSetting) string {
377+
return fmt.Sprintf(`
378+
resource "tfe_saml_settings" "foobar" {
379+
idp_cert = "%s"
380+
slo_endpoint_url = "%s"
381+
sso_endpoint_url = "%s"
382+
private_key_wo = "%s"
383+
}`, s.IDPCert, s.SLOEndpointURL, s.SSOEndpointURL, s.PrivateKey)
384+
}

website/docs/r/saml_settings.html.markdown

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ The following arguments are supported:
4444
* `sso_api_token_session_timeout` - (Optional) Specifies the Single Sign On session timeout in seconds. Defaults to 14 days.
4545
* `certificate` - (Optional) The certificate used for request and assertion signing.
4646
* `private_key` - (Optional) The private key used for request and assertion signing.
47+
* `private_key_wo` - (Optional) The private key used for request and assertion signing, guaranteed not to be written to plan or state artifacts. One of `private_key` or `private_key_wo` must be provided.
4748
* `signature_signing_method` - (Optional) Signature Signing Method. Must be either `SHA1` or `SHA256`. Defaults to `SHA256`.
4849
* `signature_digest_method` - (Optional) Signature Digest Method. Must be either `SHA1` or `SHA256`. Defaults to `SHA256`.
4950

51+
-> **Note:** Write-Only argument `private_key_wo` is available to use in place of `private_key`. 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).
52+
5053
## Attributes Reference
5154

5255
* `id` - The ID of the SAML Settings. Always `saml`.

0 commit comments

Comments
 (0)