From a3b6dd5a03e6cab47c0babcdc655f92dc194ab43 Mon Sep 17 00:00:00 2001 From: Jarrett Spiker Date: Wed, 24 Sep 2025 14:51:14 -0400 Subject: [PATCH 1/2] Add user_token_enabled attribute to organizations --- internal/provider/provider_test.go | 13 +++ .../provider/resource_tfe_organization.go | 14 +++ .../resource_tfe_organization_test.go | 99 +++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e9699a14d..9ef595ef1 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -157,6 +157,19 @@ func getClientUsingEnv() (*tfe.Client, error) { return tfeClient, nil } +func getClientWithToken(token string) (*tfe.Client, error) { + hostname := client.DefaultHostname + if os.Getenv("TFE_HOSTNAME") != "" { + hostname = os.Getenv("TFE_HOSTNAME") + } + + tfeClient, err := client.GetClient(hostname, token, defaultSSLSkipVerify) + if err != nil { + return nil, fmt.Errorf("Error getting client: %w", err) + } + return tfeClient, nil +} + func TestProvider(t *testing.T) { if err := Provider().InternalValidate(); err != nil { t.Fatalf("err: %s", err) diff --git a/internal/provider/resource_tfe_organization.go b/internal/provider/resource_tfe_organization.go index 4c3eded0c..fce7b94fe 100644 --- a/internal/provider/resource_tfe_organization.go +++ b/internal/provider/resource_tfe_organization.go @@ -108,6 +108,11 @@ func resourceTFEOrganization() *schema.Resource { Optional: true, Default: true, }, + "user_tokens_enabled": { + Type: schema.TypeBool, + Default: true, + Optional: true, + }, "enforce_hyok": { Type: schema.TypeBool, @@ -172,6 +177,10 @@ func resourceTFEOrganizationRead(d *schema.ResourceData, meta interface{}) error d.Set("speculative_plan_management_enabled", org.SpeculativePlanManagementEnabled) d.Set("enforce_hyok", org.EnforceHYOK) + if org.UserTokensEnabled != nil { + d.Set("user_tokens_enabled", org.UserTokensEnabled) + } + if org.DefaultProject != nil { d.Set("default_project_id", org.DefaultProject.ID) } @@ -240,6 +249,11 @@ func resourceTFEOrganizationUpdate(d *schema.ResourceData, meta interface{}) err options.SpeculativePlanManagementEnabled = tfe.Bool(speculativePlanManagementEnabled.(bool)) } + // If user_tokens_enabled is supplied, set it using the options struct. + if userTokensEnabled, ok := d.GetOkExists("user_tokens_enabled"); ok { + options.UserTokensEnabled = tfe.Bool(userTokensEnabled.(bool)) + } + // If enforce_hyok is supplied, set it using the options struct. if enforceHYOK, ok := d.GetOkExists("enforce_hyok"); ok { options.EnforceHYOK = tfe.Bool(enforceHYOK.(bool)) diff --git a/internal/provider/resource_tfe_organization_test.go b/internal/provider/resource_tfe_organization_test.go index 8b2d143c6..de5e9f51f 100644 --- a/internal/provider/resource_tfe_organization_test.go +++ b/internal/provider/resource_tfe_organization_test.go @@ -85,6 +85,8 @@ func TestAccTFEOrganization_full(t *testing.T) { "tfe_organization.foobar", "allow_force_delete_workspaces", "false"), resource.TestCheckResourceAttr( "tfe_organization.foobar", "speculative_plan_management_enabled", "true"), + resource.TestCheckResourceAttr( + "tfe_organization.foobar", "user_tokens_enabled", "true"), ), }, }, @@ -205,6 +207,80 @@ func TestAccTFEOrganization_update_costEstimation(t *testing.T) { }) } +func TestAccTFEOrganization_user_tokens_enabled(t *testing.T) { + // this test is a bit tricky because once user tokens are disabled, we cannot use a user token to re-enable them + // through the API. + // Therefore, we need to create an org, generate an owners team token for that org, and then use that token + // in the go-tfe client to test the user_tokens_enabled setting. + + org := &tfe.Organization{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEOrganization_basic(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEOrganizationExists( + "tfe_organization.foobar", org), + testAccCheckTFEOrganizationAttributesBasic(org, orgName), + resource.TestCheckResourceAttr( + "tfe_organization.foobar", "user_tokens_enabled", "true"), + ), + }, + { + PreConfig: func() { + // create a team token for the owners team in the org, + // then set the provider token to that value + ownersTeams, err := testAccConfiguredClient.Client.Teams.List(ctx, org.Name, &tfe.TeamListOptions{ + Names: []string{"owners"}, + }) + if err != nil { + t.Fatal(err) + } + if len(ownersTeams.Items) != 1 { + t.Fatalf("expected to find 1 owners team, found %d", len(ownersTeams.Items)) + } + ownersTeam := ownersTeams.Items[0] + + teamToken, err := testAccConfiguredClient.Client.TeamTokens.Create(ctx, ownersTeam.ID) + if err != nil { + t.Fatal(err) + } + + tfeClient, err := getClientWithToken(teamToken.ID) + if err != nil { + t.Fatal(err) + } + + testAccConfiguredClient = &ConfiguredClient{ + Client: tfeClient, + Organization: org.Name, + } + }, + Config: testAccTFEOrganization_userTokensEnabled(rInt, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "tfe_organization.foobar", "user_tokens_enabled", "false"), + testAccCheckTFEOrganizationUserTokensEnabled(org, orgName, false), + ), + }, + { + Config: testAccTFEOrganization_userTokensEnabled(rInt, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "tfe_organization.foobar", "user_tokens_enabled", "true"), + testAccCheckTFEOrganizationUserTokensEnabled(org, orgName, true), + ), + }, + }, + }) +} + func TestAccTFEOrganization_EnforceHYOK(t *testing.T) { skipUnlessHYOKEnabled(t) @@ -381,6 +457,20 @@ func testAccCheckTFEOrganizationAttributesFull( } } +func testAccCheckTFEOrganizationUserTokensEnabled( + org *tfe.Organization, expectedOrgName string, expectedUserTokensEnabled bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if org.Name != expectedOrgName { + return fmt.Errorf("Bad name: %s", org.Name) + } + + if org.UserTokensEnabled != nil && *org.UserTokensEnabled != expectedUserTokensEnabled { + return fmt.Errorf("Bad user tokens enabled: %v", org.UserTokensEnabled) + } + return nil + } +} + func testAccCheckTFEOrganizationAttributesUpdated( org *tfe.Organization, expectedOrgName string, expectedCostEstimationEnabled bool) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -480,6 +570,15 @@ resource "tfe_organization" "foobar" { }`, orgName, orgEmail, costEstimationEnabled, assessmentsEnforced, allowForceDeleteWorkspaces) } +func testAccTFEOrganization_userTokensEnabled(rInt int, userTokensEnabled bool) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" + user_tokens_enabled = %t +}`, rInt, userTokensEnabled) +} + func testAccTFEOrganization_updateEnforceHYOK(orgName string, enforceHYOK bool) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { From 0583bcf74d95531c637550f5f3efc49629d60e9b Mon Sep 17 00:00:00 2001 From: Jarrett Spiker Date: Tue, 25 Nov 2025 14:34:56 -0500 Subject: [PATCH 2/2] Use version of go-tfe with necessary changes --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index fe1619006..a201f210b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-slug v0.16.8 - github.com/hashicorp/go-tfe v1.94.0 + github.com/hashicorp/go-tfe v1.96.1-0.20251125191511-09dc30b122ae github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.24.0 // indirect @@ -32,7 +32,7 @@ require ( golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - golang.org/x/time v0.12.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index d80f1263d..956d42439 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/hashicorp/go-tfe v1.93.0 h1:hJubwn1xNCo1iBO66iQkjyC+skR61cK1AQUj4O9vv github.com/hashicorp/go-tfe v1.93.0/go.mod h1:QwqgCD5seztgp76CP7F0POJPflQNSqjIvBpVohg9X50= github.com/hashicorp/go-tfe v1.94.0 h1:EjUo5kEV5m9BSfAduSbttGfR4M2TE0O5vJV3RNBArdw= github.com/hashicorp/go-tfe v1.94.0/go.mod h1:hTnfAzkwOMvWL4sVKNPzUYTjrbwKIWnRWYSIC/Zh5SA= +github.com/hashicorp/go-tfe v1.96.1-0.20251125191511-09dc30b122ae h1:+859fnZn2SNtprSGs2jfhnLp3+skaHITeGwwYmBhf0k= +github.com/hashicorp/go-tfe v1.96.1-0.20251125191511-09dc30b122ae/go.mod h1:umRhpwmiMAa5Dhu8dzF0itJfBZHJPoTmS8BpNZs9+2Y= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -243,6 +245,8 @@ golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=