Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
FEATURES:
* Added optional description for user token roles to provide a description in HCPTF UI: https://github.com/hashicorp/vault-plugin-secrets-terraform/pull/84

* Added support for HCP TF multiple team tokens. Introduced new optional parameter `credential_type` that can be used with value `team` to issue multiple team tokens. Tokens can optionally have a ttl and max_ttl that is respected via normal lease operations as well as Expired by HCP TF API at their expected time: https://github.com/hashicorp/vault-plugin-secrets-terraform/pull/89

## 0.11.0
### FEb 7, 2025
IMPROVEMENTS:
Expand Down
63 changes: 61 additions & 2 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (e *testEnv) ReadOrgToken(t *testing.T) {
require.NotNil(t, ot)
}

func (e *testEnv) AddTeamTokenRole(t *testing.T) {
func (e *testEnv) AddTeamLegacyTokenRole(t *testing.T) {
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/test-team-token",
Expand All @@ -128,7 +128,7 @@ func (e *testEnv) AddTeamTokenRole(t *testing.T) {
require.Nil(t, resp)
}

func (e *testEnv) ReadTeamToken(t *testing.T) {
func (e *testEnv) ReadTeamLegacyToken(t *testing.T) {
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/test-team-token",
Expand All @@ -155,6 +155,65 @@ func (e *testEnv) ReadTeamToken(t *testing.T) {
}
}

func (e *testEnv) AddMultiTeamTokenRole(t *testing.T) {
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/test-multiteam-token",
Storage: e.Storage,
Data: map[string]interface{}{
"team_id": e.TeamID,
"description": e.Description,
"credential_type": "team",
"ttl": "1m",
},
}
resp, err := e.Backend.HandleRequest(e.Context, req)
require.Nil(t, resp)
require.Nil(t, err)
}

func (e *testEnv) ReadMultiTeamToken(t *testing.T) {
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/test-multiteam-token",
Storage: e.Storage,
}
resp, err := e.Backend.HandleRequest(e.Context, req)
require.Nil(t, err)
require.NotNil(t, resp)
if t, ok := resp.Data["token_id"]; ok {
e.TokenIDs = append(e.TokenIDs, t.(string))
}
require.NotEmpty(t, resp.Data["token"])

if e.SecretToken != "" {
require.NotEqual(t, e.SecretToken, resp.Data["token"])
}

// collect secret IDs to revoke at end of test
require.NotNil(t, resp.Secret)
if t, ok := resp.Secret.InternalData["token_id"]; ok {
e.SecretToken = t.(string)
}
}

func (e *testEnv) CleanupMultiTeamTokens(t *testing.T) {
if len(e.TokenIDs) == 0 {
t.Fatalf("expected 2 tokens, got: %d", len(e.TokenIDs))
}

for _, id := range e.TokenIDs {
b := e.Backend.(*tfBackend)
client, err := b.getClient(e.Context, e.Storage)
if err != nil {
t.Fatal("fatal getting client")
}
if err := client.TeamTokens.DeleteByID(e.Context, id); err != nil {
t.Fatalf("unexpected error deleting multiteam token: %s", err)
}
}
}

func (e *testEnv) AddUserTokenRole(t *testing.T) {
req := &logical.Request{
Operation: logical.UpdateOperation,
Expand Down
8 changes: 5 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package tfc

import (
"errors"
"time"

"github.com/hashicorp/go-tfe"
)
Expand All @@ -14,9 +15,10 @@ type client struct {
}

type terraformToken struct {
ID string `json:"id"`
Description string `json:"description"`
Token string `json:"token"`
ID string `json:"id"`
Description string `json:"description"`
Token string `json:"token"`
ExpiredAt time.Time `json:"expired_at,omitempty"`
}

func newClient(config *tfConfig) (*client, error) {
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ toolchain go1.22.0

require (
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-tfe v1.74.1
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2
github.com/hashicorp/go-tfe v1.78.0
github.com/hashicorp/vault/api v1.14.0
github.com/hashicorp/vault/sdk v0.13.0
github.com/stretchr/testify v1.10.0
Expand Down Expand Up @@ -45,14 +46,13 @@ require (
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.3.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-slug v0.16.3 // indirect
github.com/hashicorp/go-slug v0.16.4 // indirect
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/jsonapi v1.3.2 // indirect
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/kr/pretty v0.3.1 // indirect
Expand Down Expand Up @@ -86,7 +86,7 @@ require (
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ github.com/hashicorp/go-secure-stdlib/plugincontainer v0.3.0 h1:KMWpBsC65ZBXDpox
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.3.0/go.mod h1:qKYwSZ2EOpppko5ud+Sh9TrUgiTAZSaQCr8XWIYXsbM=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns=
github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ=
github.com/hashicorp/go-slug v0.16.4 h1:kI0mOUVjbBsyocwO29pZIQzzkBnfQNdU4eqlUpNdNVA=
github.com/hashicorp/go-slug v0.16.4/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ=
github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-tfe v1.74.1 h1:I/8fOwSYox17IZV7SULIQH0ZRPNL2g/biW6hHWnOTVY=
github.com/hashicorp/go-tfe v1.74.1/go.mod h1:kGHWMZ3HHjitgqON8nBZ4kPVJ3cLbzM4JMgmNVMs9aQ=
github.com/hashicorp/go-tfe v1.78.0 h1:RMkrEO3N4hbnXqoMWl44TnSCkMXpON5iEOOJf+UxWAo=
github.com/hashicorp/go-tfe v1.78.0/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA=
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=
Expand All @@ -133,8 +133,8 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs=
github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU=
github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk=
github.com/hashicorp/vault/sdk v0.13.0 h1:UmcLF+7r70gy1igU44Suflgio30P2GOL4MkHPhJuiP8=
Expand Down Expand Up @@ -311,8 +311,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
Expand Down
21 changes: 15 additions & 6 deletions path_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func (b *tfBackend) pathCredentialsRead(ctx context.Context, req *logical.Reques
return nil, errors.New("error retrieving role: role is nil")
}

if roleEntry.UserID != "" {
return b.createUserCreds(ctx, req, roleEntry)
if roleEntry.CredentialType == userCredentialType || roleEntry.CredentialType == teamCredentialType {
return b.createUserOrMultiTeamCreds(ctx, req, roleEntry)
}

resp := &logical.Response{
Expand All @@ -92,7 +92,7 @@ func (b *tfBackend) pathCredentialsRead(ctx context.Context, req *logical.Reques
return resp, nil
}

func (b *tfBackend) createUserCreds(ctx context.Context, req *logical.Request, role *terraformRoleEntry) (*logical.Response, error) {
func (b *tfBackend) createUserOrMultiTeamCreds(ctx context.Context, req *logical.Request, role *terraformRoleEntry) (*logical.Response, error) {
token, err := b.createToken(ctx, req.Storage, role)
if err != nil {
return nil, err
Expand All @@ -103,8 +103,12 @@ func (b *tfBackend) createUserCreds(ctx context.Context, req *logical.Request, r
"token_id": token.ID,
}

if role.Description != "" {
data["description"] = role.Description
if token.Description != "" {
data["description"] = token.Description
}

if !token.ExpiredAt.IsZero() {
data["expired_at"] = token.ExpiredAt
}

resp := b.Secret(terraformTokenType).Response(data, map[string]interface{}{
Expand Down Expand Up @@ -135,7 +139,12 @@ func (b *tfBackend) createToken(ctx context.Context, s logical.Storage, roleEntr
case isOrgToken(roleEntry.Organization, roleEntry.TeamID):
token, err = createOrgToken(ctx, client, roleEntry.Organization)
case isTeamToken(roleEntry.TeamID):
token, err = createTeamToken(ctx, client, roleEntry.TeamID)
if roleEntry.CredentialType == teamCredentialType {
token, err = createTeamTokenWithOptions(ctx, client, *roleEntry, b.System().MaxLeaseTTL())
} else {
// team_legacy tokens
token, err = createTeamLegacyToken(ctx, client, roleEntry.TeamID)
}
default:
token, err = createUserToken(ctx, client, roleEntry.UserID, roleEntry.Description)
}
Expand Down
24 changes: 21 additions & 3 deletions path_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func newAcceptanceTestEnv() (*testEnv, error) {
Organization: os.Getenv(envVarTerraformOrganization),
TeamID: os.Getenv(envVarTerraformTeamID),
UserID: os.Getenv(envVarTerraformUserID),
Description: "acc-test",
Backend: b,
Context: ctx,
Storage: &logical.InmemStorage{},
Expand All @@ -56,7 +57,7 @@ func TestAcceptanceOrganizationToken(t *testing.T) {
t.Run("read organization token cred", acceptanceTestEnv.ReadOrgToken)
}

func TestAcceptanceTeamToken(t *testing.T) {
func TestAcceptanceTeamLegacyToken(t *testing.T) {
if !runAcceptanceTests {
t.SkipNow()
}
Expand All @@ -67,8 +68,25 @@ func TestAcceptanceTeamToken(t *testing.T) {
}

t.Run("add config", acceptanceTestEnv.AddConfig)
t.Run("add team token role", acceptanceTestEnv.AddTeamTokenRole)
t.Run("read team token cred", acceptanceTestEnv.ReadTeamToken)
t.Run("add team token role", acceptanceTestEnv.AddTeamLegacyTokenRole)
t.Run("read team token cred", acceptanceTestEnv.ReadTeamLegacyToken)
}

func TestAcceptanceMultiTeamToken(t *testing.T) {
if !runAcceptanceTests {
t.SkipNow()
}

acceptanceTestEnv, err := newAcceptanceTestEnv()
if err != nil {
t.Fatal(err)
}

t.Run("add config", acceptanceTestEnv.AddConfig)
t.Run("add multiteam token role", acceptanceTestEnv.AddMultiTeamTokenRole)
t.Run("read multiteam token cred", acceptanceTestEnv.ReadMultiTeamToken)
t.Run("read multiteam token cred", acceptanceTestEnv.ReadMultiTeamToken)
t.Run("cleanup multiteam tokens", acceptanceTestEnv.CleanupMultiTeamTokens)
}

func TestAcceptanceUserToken(t *testing.T) {
Expand Down
Loading