diff --git a/CHANGELOG.md b/CHANGELOG.md index 9851be67e..8ea66d231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # UNRELEASED +## Enhancements + +* Adds support for creating different organization toke types by @glennsarti [#943](https://github.com/hashicorp/go-tfe/pull/943) + # v1.61.0 ## Enhancements diff --git a/mocks/organization_token_mocks.go b/mocks/organization_token_mocks.go index 404152f1b..65bef252e 100644 --- a/mocks/organization_token_mocks.go +++ b/mocks/organization_token_mocks.go @@ -84,6 +84,20 @@ func (mr *MockOrganizationTokensMockRecorder) Delete(ctx, organization any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOrganizationTokens)(nil).Delete), ctx, organization) } +// DeleteWithOptions mocks base method. +func (m *MockOrganizationTokens) DeleteWithOptions(ctx context.Context, organization string, options tfe.OrganizationTokenDeleteOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteWithOptions", ctx, organization, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteWithOptions indicates an expected call of DeleteWithOptions. +func (mr *MockOrganizationTokensMockRecorder) DeleteWithOptions(ctx, organization, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWithOptions", reflect.TypeOf((*MockOrganizationTokens)(nil).DeleteWithOptions), ctx, organization, options) +} + // Read mocks base method. func (m *MockOrganizationTokens) Read(ctx context.Context, organization string) (*tfe.OrganizationToken, error) { m.ctrl.T.Helper() @@ -98,3 +112,18 @@ func (mr *MockOrganizationTokensMockRecorder) Read(ctx, organization any) *gomoc mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOrganizationTokens)(nil).Read), ctx, organization) } + +// ReadWithOptions mocks base method. +func (m *MockOrganizationTokens) ReadWithOptions(ctx context.Context, organization string, options tfe.OrganizationTokenReadOptions) (*tfe.OrganizationToken, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadWithOptions", ctx, organization, options) + ret0, _ := ret[0].(*tfe.OrganizationToken) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadWithOptions indicates an expected call of ReadWithOptions. +func (mr *MockOrganizationTokensMockRecorder) ReadWithOptions(ctx, organization, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockOrganizationTokens)(nil).ReadWithOptions), ctx, organization, options) +} diff --git a/organization_token.go b/organization_token.go index 68dd02c25..16611cd6f 100644 --- a/organization_token.go +++ b/organization_token.go @@ -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. // @@ -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. @@ -53,6 +67,36 @@ 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 +} + +// OrganizationTokenDeleteOptions contains the options for deleting 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 { + params := url.Values{ + "token": {*tokenType}, + } + u = u + "?" + encodeQueryParams(params) + } + return u } // Create a new organization token, replacing any existing token. @@ -66,7 +110,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 @@ -83,11 +127,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 @@ -104,11 +153,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 diff --git a/organization_token_integration_test.go b/organization_token_integration_test.go index 61ae81a2f..6a80ab096 100644 --- a/organization_token_integration_test.go +++ b/organization_token_integration_test.go @@ -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) { @@ -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) { @@ -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() @@ -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()) + }) +}