Skip to content

Commit

Permalink
Merge pull request #943 from hashicorp/gs/add-audit-tokens
Browse files Browse the repository at this point in the history
Add support for Org. Token types
  • Loading branch information
glennsarti authored Aug 16, 2024
2 parents 33407ff + b9a6e7e commit 6ad8729
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 @@ -5,6 +5,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 [#957](https://github.com/hashicorp/go-tfe/pull/957) and @DanielMSchmidt [#960](https://github.com/hashicorp/go-tfe/pull/960)
* 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 6ad8729

Please sign in to comment.