Skip to content

Commit

Permalink
feat: add service user token resource
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszjenek committed Apr 17, 2024
1 parent 21dff26 commit 2eb84d5
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 5 deletions.
45 changes: 45 additions & 0 deletions docs/resources/service_user_token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "humanitec_service_user_token Resource - terraform-provider-humanitec"
subcategory: ""
description: |-
Tokens can be generated from service users and are used to interact with Humanitec’s API on the service user’s behalf.
---

# humanitec_service_user_token (Resource)

Tokens can be generated from service users and are used to interact with Humanitec’s API on the service user’s behalf.

## Example Usage

```terraform
resource "humanitec_user" "service_user" {
name = "example-service-user"
role = "administrator"
type = "service"
}
resource "humanitec_service_user_token" "token" {
id = "example-service-token"
user_id = humanitec_user.service_user.id
description = "example token description"
expires_at = "2024-04-23T21:59:59.999Z"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `id` (String) Identifier of the token. Must be unique for the user.
- `user_id` (String) The service user ID.

### Optional

- `description` (String) A description of the token.
- `expires_at` (String) The time the token expires. If not set, the token will not expire.

### Read-Only

- `token` (String) Unique token granting access to specific services within the platform.
12 changes: 12 additions & 0 deletions examples/resources/humanitec_service_user_token/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
resource "humanitec_user" "service_user" {
name = "example-service-user"
role = "administrator"
type = "service"
}

resource "humanitec_service_user_token" "token" {
id = "example-service-token"
user_id = humanitec_user.service_user.id
description = "example token description"
expires_at = "2024-04-23T21:59:59.999Z"
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res
NewResourceResourceDriver,
NewResourceRule,
NewResourceSecretStore,
NewResourceServiceUserToken,
NewResourceValue,
NewResourceUser,
NewResourceWebhook,
Expand Down
210 changes: 210 additions & 0 deletions internal/provider/resource_service_user_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/humanitec/humanitec-go-autogen"
"github.com/humanitec/humanitec-go-autogen/client"
)

// Ensure provider defined types fully satisfy framework interfaces
var _ resource.Resource = &ResourceServiceUserToken{}

func NewResourceServiceUserToken() resource.Resource {
return &ResourceServiceUserToken{}
}

// ResourceServiceUserToken defines the resource implementation.
type ResourceServiceUserToken struct {
client *humanitec.Client
orgId string
}

// ServiceUserTokenModel describes the app data model.
type ServiceUserTokenModel struct {
ID types.String `tfsdk:"id"`
UserID types.String `tfsdk:"user_id"`
Description types.String `tfsdk:"description"`
ExpiresAt types.String `tfsdk:"expires_at"`
Token types.String `tfsdk:"token"`
}

func (r *ResourceServiceUserToken) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_service_user_token"
}

func (r *ResourceServiceUserToken) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Tokens can be generated from service users and are used to interact with Humanitec’s API on the service user’s behalf.",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Identifier of the token. Must be unique for the user.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"user_id": schema.StringAttribute{
MarkdownDescription: "The service user ID.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"description": schema.StringAttribute{
MarkdownDescription: "A description of the token.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"expires_at": schema.StringAttribute{
MarkdownDescription: "The time the token expires. If not set, the token will not expire.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"token": schema.StringAttribute{
MarkdownDescription: "Unique token granting access to specific services within the platform.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}

func (r *ResourceServiceUserToken) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

resdata, ok := req.ProviderData.(*HumanitecData)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = resdata.Client
r.orgId = resdata.OrgID
}

func (r *ResourceServiceUserToken) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *ServiceUserTokenModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

id := data.ID.ValueString()
userId := data.UserID.ValueString()
description := data.Description.ValueStringPointer()
expiresAt := data.ExpiresAt.ValueStringPointer()

httpResp, err := r.client.CreateUserTokenWithResponse(ctx, userId, client.TokenDefinitionRequest{
Description: description,
ExpiresAt: expiresAt,
Id: id,
Type: "static",
})
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create service user token, got error: %s", err))
return
}
if httpResp.StatusCode() != 200 {
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create service user token, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
return
}

data.Token = types.StringValue(httpResp.JSON200.Token)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceServiceUserToken) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *ServiceUserTokenModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

id := data.ID.ValueString()
userId := data.UserID.ValueString()

httpResp, err := r.client.GetUserTokenWithResponse(ctx, userId, id)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read service user token, got error: %s", err))
return
}
if httpResp.StatusCode() != 200 {
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read service user token, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
return
}

parseTokenInfoResponse(httpResp.JSON200, data)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceServiceUserToken) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.AddError("UNSUPPORTED_OPERATION", "Updating a service user token is not supported")
}

func (r *ResourceServiceUserToken) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *ServiceUserTokenModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

userId := data.UserID.ValueString()
id := data.ID.ValueString()

httpResp, err := r.client.DeleteUserTokenWithResponse(ctx, userId, id)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete service user token, got error: %s", err))
return
}

if httpResp.StatusCode() != 204 {
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete service user token, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
return
}
}

func parseTokenInfoResponse(res *client.TokenInfoResponse, data *ServiceUserTokenModel) {
if res.Description != "" {
data.Description = types.StringPointerValue(&res.Description)
}
if res.ExpiresAt != nil && *res.ExpiresAt != "0001-01-01T00:00:00Z" {
data.ExpiresAt = types.StringPointerValue(res.ExpiresAt)
}
data.ID = types.StringValue(res.Id)
}
73 changes: 73 additions & 0 deletions internal/provider/resource_service_user_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package provider

import (
"fmt"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccResourceServiceUserToken(t *testing.T) {
const (
userName = "test user"
tokenId = "test-token-id"
description = "Test token description"
)
expiresAt := time.Now().Add(24 * time.Hour).Format("2006-01-02T15:04:05.999Z")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccCreateResourceServiceUserToken(userName, tokenId, "", ""),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_service_user_token.token", "id", tokenId),
resource.TestCheckResourceAttrSet("humanitec_service_user_token.token", "token"),
resource.TestCheckNoResourceAttr("humanitec_service_user_token.token", "description"),
resource.TestCheckNoResourceAttr("humanitec_service_user_token.token", "expires_at"),
),
},
// Update and Read testing
{
Config: testAccCreateResourceServiceUserToken(userName, tokenId, description, expiresAt),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_service_user_token.token", "id", tokenId),
resource.TestCheckResourceAttrSet("humanitec_service_user_token.token", "token"),
resource.TestCheckResourceAttr("humanitec_service_user_token.token", "description", description),
resource.TestCheckResourceAttr("humanitec_service_user_token.token", "expires_at", expiresAt),
),
},
// Delete testing automatically occurs in TestCase
},
})
}

func testAccCreateResourceServiceUserToken(userName, tokenId, tokenDescription, tokenExpiration string) string {
tokenDescriptionEntry := ""
if tokenDescription != "" {
tokenDescriptionEntry = fmt.Sprintf(`description = "%s"`, tokenDescription)
}
tokenExpirationEntry := ""
if tokenExpiration != "" {
tokenExpirationEntry = fmt.Sprintf(`expires_at = "%s"`, tokenExpiration)
}

return fmt.Sprintf(`
resource "humanitec_user" "service_user" {
name = "%s"
role = "administrator"
type = "service"
}
resource "humanitec_service_user_token" "token" {
id = "%s"
user_id = humanitec_user.service_user.id
%s
%s
}`,
userName, tokenId, tokenDescriptionEntry, tokenExpirationEntry,
)
}
4 changes: 2 additions & 2 deletions internal/provider/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,12 @@ func (r *ResourceUser) Delete(ctx context.Context, req resource.DeleteRequest, r
id := data.ID.ValueString()
httpResp, err := r.client.DeleteUserRoleInOrgWithResponse(ctx, r.orgId, id)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete service user, got error: %s", err))
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete user, got error: %s", err))
return
}

if httpResp.StatusCode() != 204 {
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete value, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete user, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
return
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/provider/resource_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestAccResourceUser(t *testing.T) {
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccResourceUser(name, role, userType),
Config: testAccCreateResourceUser(name, role, userType),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_user.test", "name", name),
resource.TestCheckResourceAttr("humanitec_user.test", "role", role),
Expand All @@ -40,7 +40,7 @@ func TestAccResourceUser(t *testing.T) {
},
// Update and Read testing
{
Config: testAccResourceUser(name, newRole, userType),
Config: testAccCreateResourceUser(name, newRole, userType),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_user.test", "name", name),
resource.TestCheckResourceAttr("humanitec_user.test", "role", newRole),
Expand All @@ -52,7 +52,7 @@ func TestAccResourceUser(t *testing.T) {
})
}

func testAccResourceUser(name, role, userType string) string {
func testAccCreateResourceUser(name, role, userType string) string {
return fmt.Sprintf(`
resource "humanitec_user" "test" {
name = "%s"
Expand Down

0 comments on commit 2eb84d5

Please sign in to comment.