Skip to content

Commit 14adcf9

Browse files
committed
feat: add write-only private_key_wo attribute to tfe_saml_settings
1 parent 586f314 commit 14adcf9

File tree

3 files changed

+130
-5
lines changed

3 files changed

+130
-5
lines changed

internal/provider/data_source_saml_settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type modelTFESAMLSettings struct {
5050
MetadataURL types.String `tfsdk:"metadata_url"`
5151
Certificate types.String `tfsdk:"certificate"`
5252
PrivateKey types.String `tfsdk:"private_key"`
53+
PrivateKeyWO types.String `tfsdk:"private_key_wo"`
5354
SignatureSigningMethod types.String `tfsdk:"signature_signing_method"`
5455
SignatureDigestMethod types.String `tfsdk:"signature_digest_method"`
5556
}

internal/provider/resource_tfe_saml_settings.go

Lines changed: 77 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 (
@@ -36,7 +40,7 @@ type resourceTFESAMLSettings struct {
3640
}
3741

3842
// modelFromTFEAdminSAMLSettings builds a modelTFESAMLSettings struct from a tfe.AdminSAMLSetting value
39-
func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.String) modelTFESAMLSettings {
43+
func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.String, isWriteOnly bool) modelTFESAMLSettings {
4044
m := modelTFESAMLSettings{
4145
ID: types.StringValue(v.ID),
4246
Enabled: types.BoolValue(v.Enabled),
@@ -60,9 +64,16 @@ func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.Stri
6064
SignatureSigningMethod: types.StringValue(v.SignatureSigningMethod),
6165
SignatureDigestMethod: types.StringValue(v.SignatureDigestMethod),
6266
}
67+
6368
if len(privateKey.String()) > 0 {
6469
m.PrivateKey = privateKey
6570
}
71+
72+
// Don't retrieve values if write-only is being used. Unset the hmac key field before updating the state.
73+
if isWriteOnly {
74+
m.PrivateKey = types.StringValue("")
75+
}
76+
6677
return m
6778
}
6879

@@ -187,6 +198,21 @@ func (r *resourceTFESAMLSettings) Schema(ctx context.Context, req resource.Schem
187198
Optional: true,
188199
Computed: true,
189200
Sensitive: true,
201+
Validators: []validator.String{
202+
stringvalidator.ConflictsWith(path.MatchRoot("private_key_wo")),
203+
},
204+
},
205+
"private_key_wo": schema.StringAttribute{
206+
Description: "The private key in write-only mode used for request and assertion signing",
207+
Optional: true,
208+
Sensitive: true,
209+
WriteOnly: true,
210+
Validators: []validator.String{
211+
stringvalidator.ConflictsWith(path.MatchRoot("private_key")),
212+
},
213+
PlanModifiers: []planmodifier.String{
214+
planmodifiers.NewReplaceForWriteOnlyStringValue("private_key_wo"),
215+
},
190216
},
191217
"signature_signing_method": schema.StringAttribute{
192218
Description: fmt.Sprintf("Signature Signing Method. Must be either `%s` or `%s`. Defaults to `%s`", samlSignatureMethodSHA1, samlSignatureMethodSHA256, samlSignatureMethodSHA256),
@@ -225,13 +251,22 @@ func (r *resourceTFESAMLSettings) Read(ctx context.Context, req resource.ReadReq
225251
return
226252
}
227253

254+
tflog.Debug(ctx, "Reading SAML Settings")
255+
228256
samlSettings, err := r.client.Admin.Settings.SAML.Read(ctx)
229257
if err != nil {
230258
resp.Diagnostics.AddError("Error reading SAML Settings", "Could not read SAML Settings, unexpected error: "+err.Error())
231259
return
232260
}
233261

234-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
262+
isWriteOnly, diags := r.writeOnlyValueStore(resp.Private).PriorValueExists(ctx)
263+
resp.Diagnostics.Append(diags...)
264+
if diags.HasError() {
265+
return
266+
}
267+
268+
// update state
269+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, isWriteOnly)
235270
diags = resp.State.Set(ctx, &result)
236271
resp.Diagnostics.Append(diags...)
237272
}
@@ -245,14 +280,28 @@ func (r *resourceTFESAMLSettings) Create(ctx context.Context, req resource.Creat
245280
return
246281
}
247282

283+
var config modelTFESAMLSettings
284+
diags = req.Config.Get(ctx, &config)
285+
resp.Diagnostics.Append(diags...)
286+
if resp.Diagnostics.HasError() {
287+
return
288+
}
289+
290+
if !config.PrivateKeyWO.IsNull() {
291+
m.PrivateKey = config.PrivateKeyWO
292+
}
293+
248294
tflog.Debug(ctx, "Create SAML Settings")
249295
samlSettings, err := r.updateSAMLSettings(ctx, m)
250296
if err != nil {
251297
resp.Diagnostics.AddError("Error creating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error())
252298
return
253299
}
254300

255-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
301+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, !config.PrivateKeyWO.IsNull())
302+
// Store the hashed write-only value in the private state
303+
store := r.writeOnlyValueStore(resp.Private)
304+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.PrivateKeyWO)...)
256305
diags = resp.State.Set(ctx, &result)
257306
resp.Diagnostics.Append(diags...)
258307
}
@@ -266,14 +315,33 @@ func (r *resourceTFESAMLSettings) Update(ctx context.Context, req resource.Updat
266315
return
267316
}
268317

318+
var config modelTFESAMLSettings
319+
diags = req.Config.Get(ctx, &config)
320+
resp.Diagnostics.Append(diags...)
321+
if resp.Diagnostics.HasError() {
322+
return
323+
}
324+
325+
if !config.PrivateKeyWO.IsNull() {
326+
m.PrivateKey = config.PrivateKeyWO
327+
}
328+
269329
tflog.Debug(ctx, "Update SAML Settings")
270330
samlSettings, err := r.updateSAMLSettings(ctx, m)
271331
if err != nil {
272332
resp.Diagnostics.AddError("Error updating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error())
273333
return
274334
}
275335

276-
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey)
336+
// Store the hashed write-only value in the private state
337+
store := r.writeOnlyValueStore(resp.Private)
338+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.PrivateKeyWO)...)
339+
if resp.Diagnostics.HasError() {
340+
return
341+
}
342+
343+
result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey, !config.PrivateKeyWO.IsNull())
344+
// Save data into Terraform state
277345
diags = resp.State.Set(ctx, &result)
278346
resp.Diagnostics.Append(diags...)
279347
}
@@ -321,7 +389,7 @@ func (r *resourceTFESAMLSettings) ImportState(ctx context.Context, req resource.
321389
return
322390
}
323391

324-
result := modelFromTFEAdminSAMLSettings(*samlSettings, types.StringValue(""))
392+
result := modelFromTFEAdminSAMLSettings(*samlSettings, types.StringValue(""), false)
325393
diags := resp.State.Set(ctx, &result)
326394
resp.Diagnostics.Append(diags...)
327395
}
@@ -363,3 +431,7 @@ func (r *resourceTFESAMLSettings) updateSAMLSettings(ctx context.Context, m mode
363431
}
364432
return s, nil
365433
}
434+
435+
func (r *resourceTFESAMLSettings) writeOnlyValueStore(private helpers.PrivateState) *helpers.WriteOnlyValueStore {
436+
return helpers.NewWriteOnlyValueStore(private, "private_key_wo")
437+
}

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+
}

0 commit comments

Comments
 (0)