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 14, 2024
1 parent 564a1e1 commit a818252
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Enhancements

* Adds more BETA support for `Stacks` resources, which is is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc
* Adds support for creating different organization token types by @glennsarti [#943](https://github.com/hashicorp/go-tfe/pull/943)

# v1.62.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.

59 changes: 55 additions & 4 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 @@ -52,7 +66,24 @@ type OrganizationToken struct {
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"`
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty" url:"-"`
// Optional: What type of token to create
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType `url:"token,omitempty"`
}

// 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 `url:"token,omitempty"`
}

// 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 `url:"token,omitempty"`
}

// Create a new organization token, replacing any existing token.
Expand All @@ -67,7 +98,12 @@ func (s *organizationTokens) CreateWithOptions(ctx context.Context, organization
}

u := fmt.Sprintf("organizations/%s/authentication-token", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
qp, err := decodeQueryParams(options)
if err != nil {
return nil, err
}

req, err := s.client.NewRequestWithAdditionalQueryParams("POST", u, &options, qp)
if err != nil {
return nil, err
}
Expand All @@ -83,12 +119,17 @@ 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.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
Expand All @@ -104,12 +145,22 @@ 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.PathEscape(organization))
req, err := s.client.NewRequest("DELETE", u, nil)
qp, err := decodeQueryParams(options)
if err != nil {
return err
}

req, err := s.client.NewRequestWithAdditionalQueryParams("DELETE", u, nil, qp)
if err != nil {
return err
}
Expand Down
91 changes: 91 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,48 @@ 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)

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

// Delete it
deleteOptions := OrganizationTokenDeleteOptions{
TokenType: &tt,
}
err = client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
require.NoError(t, err)

// Reload the token
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{
TokenType: &tt,
})
// ... it should fail
assert.Nil(t, ot)
assert.Equal(t, err, ErrResourceNotFound)

// Delete it again
err = client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
// ... it should fail
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("without valid organization", func(t *testing.T) {
deleteOptions := OrganizationTokenDeleteOptions{}
err := client.OrganizationTokens.DeleteWithOptions(ctx, badIdentifier, deleteOptions)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
11 changes: 11 additions & 0 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,17 @@ func encodeQueryParams(v url.Values) string {
return buf.String()
}

// decodeQueryParams types an object and converts the struct fields into
// Query Parameters, which can be used with NewRequestWithAdditionalQueryParams
// Note that a field without a `url` annotation will be converted into a query
// parameter. Use url:"-" to ignore struct fields.
func decodeQueryParams(v any) (url.Values, error) {
if v == nil {
return make(url.Values, 0), nil
}
return query.Values(v)
}

// serializeRequestBody serializes the given ptr or ptr slice into a JSON
// request. It automatically uses jsonapi or json serialization, depending
// on the body type's tags.
Expand Down

0 comments on commit a818252

Please sign in to comment.