Skip to content

Commit

Permalink
Add support for Org. Token types
Browse files Browse the repository at this point in the history
HCP Terraform organizations now support different types of tokens. This
commit updates the org. auth. token client to support CRUD operations with
a token type URL attribute.

Note that the integration tests require an organization that supports
audit-logging
  • Loading branch information
glennsarti committed Aug 1, 2024
1 parent de2e5b8 commit 97a621f
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# UNRELEASED

## Enhancements

* Adds `AllowMemberTokenManagement` permission to `Team` by @juliannatetreault [#922](https://github.com/hashicorp/go-tfe/pull/922)
* Adds support for creating different organization token types by @glennsarti [#943](https://github.com/hashicorp/go-tfe/pull/943)

# v1.61.0

Expand Down
29 changes: 29 additions & 0 deletions mocks/organization_token_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 57 additions & 3 deletions organization_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import (
// Compile-time proof of interface implementation.
var _ OrganizationTokens = (*organizationTokens)(nil)

type TokenType string

const (
// A token which can only access the Audit Trails of an HCP Terraform Organization.
// See https://developer.hashicorp.com/terraform/cloud-docs/api-docs/audit-trails-tokens
AuditTrailToken TokenType = "audit-trails"
)

// OrganizationTokens describes all the organization token related methods
// that the Terraform Enterprise API supports.
//
Expand All @@ -28,8 +36,14 @@ type OrganizationTokens interface {
// Read an organization token.
Read(ctx context.Context, organization string) (*OrganizationToken, error)

// Read an organization token with options.
ReadWithOptions(ctx context.Context, organization string, options OrganizationTokenReadOptions) (*OrganizationToken, error)

// Delete an organization token.
Delete(ctx context.Context, organization string) error

// Delete an organization token with options.
DeleteWithOptions(ctx context.Context, organization string, options OrganizationTokenDeleteOptions) error
}

// organizationTokens implements OrganizationTokens.
Expand All @@ -53,6 +67,35 @@ type OrganizationTokenCreateOptions struct {
// Optional: The token's expiration date.
// This feature is available in TFE release v202305-1 and later
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty"`
// Optional: What type of token to create
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType
}

// OrganizationTokenReadOptions contains the options for reading an organization token.
type OrganizationTokenReadOptions struct {
// Optional: What type of token to read
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType
}

// OrganizationTokenDeleteOptions contains the options for deleting an organization token.
type OrganizationTokenDeleteOptions struct {
// Optional: What type of token to delete
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType
}

// Creates the URL for use against organization token API routes
func (s *organizationTokens) generateURL(organization string, tokenType *TokenType) string {
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
// Append the token type to the URL
if tokenType != nil {
u = u + "?" + encodeQueryParams(url.Values{
"token": {*tokenType},
})
}
return u
}

// Create a new organization token, replacing any existing token.
Expand All @@ -66,7 +109,7 @@ func (s *organizationTokens) CreateWithOptions(ctx context.Context, organization
return nil, ErrInvalidOrg
}

u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
u := s.generateURL(organization, options.TokenType)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
Expand All @@ -83,11 +126,16 @@ func (s *organizationTokens) CreateWithOptions(ctx context.Context, organization

// Read an organization token.
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
return s.ReadWithOptions(ctx, organization, OrganizationTokenReadOptions{})
}

// Read an organization token with options.
func (s *organizationTokens) ReadWithOptions(ctx context.Context, organization string, options OrganizationTokenReadOptions) (*OrganizationToken, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}

u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
u := s.generateURL(organization, options.TokenType)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
Expand All @@ -104,11 +152,17 @@ func (s *organizationTokens) Read(ctx context.Context, organization string) (*Or

// Delete an organization token.
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
return s.DeleteWithOptions(ctx, organization, OrganizationTokenDeleteOptions{})
}

// Delete an organization token with options
func (s *organizationTokens) DeleteWithOptions(ctx context.Context, organization string, options OrganizationTokenDeleteOptions) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}

u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
u := s.generateURL(organization, options.TokenType)

req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
Expand Down
80 changes: 80 additions & 0 deletions organization_token_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func TestOrganizationTokens_CreateWithOptions(t *testing.T) {

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)

var tkToken string
t.Run("with valid options", func(t *testing.T) {
Expand Down Expand Up @@ -89,6 +91,16 @@ func TestOrganizationTokens_CreateWithOptions(t *testing.T) {
assert.Equal(t, ot.ExpiredAt, oneDayLater)
tkToken = ot.Token
})

t.Run("with a token type", func(t *testing.T) {
tt := AuditTrailToken
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{
TokenType: &tt,
})

require.NoError(t, err)
require.NotEmpty(t, ot.Token)
})
}

func TestOrganizationTokensRead(t *testing.T) {
Expand Down Expand Up @@ -135,6 +147,40 @@ func TestOrganizationTokensRead(t *testing.T) {
})
}

func TestOrganizationTokensReadWithOptions(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)

tt := AuditTrailToken
noTypeToken, _ := createOrganizationToken(t, client, orgTest)
auditTypeToken, _ := createOrganizationTokenWithOptions(t, client, orgTest, OrganizationTokenCreateOptions{TokenType: &tt})

t.Run("with empty options", func(t *testing.T) {
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{})
require.NoError(t, err)
assert.NotEmpty(t, ot)
assert.Equal(t, ot.ID, noTypeToken.ID)
})

t.Run("with a specific token type", func(t *testing.T) {
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{TokenType: &tt})
require.NoError(t, err)
assert.NotEmpty(t, ot)
assert.Equal(t, ot.ID, auditTypeToken.ID)
})

t.Run("without valid organization", func(t *testing.T) {
ot, err := client.OrganizationTokens.Read(ctx, badIdentifier)
assert.Nil(t, ot)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}

func TestOrganizationTokensDelete(t *testing.T) {
client := testClient(t)
ctx := context.Background()
Expand All @@ -159,3 +205,37 @@ func TestOrganizationTokensDelete(t *testing.T) {
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}

func TestOrganizationTokensDeleteWithOptions(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)

tt := AuditTrailToken
createOrganizationTokenWithOptions(t, client, orgTest, OrganizationTokenCreateOptions{
TokenType: &tt,
})

deleteOptions := OrganizationTokenDeleteOptions{
TokenType: &tt,
}

t.Run("with valid options", func(t *testing.T) {
err := client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
require.NoError(t, err)
})

t.Run("when a token does not exist", func(t *testing.T) {
err := client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("without valid organization", func(t *testing.T) {
err := client.OrganizationTokens.DeleteWithOptions(ctx, badIdentifier, deleteOptions)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}

0 comments on commit 97a621f

Please sign in to comment.