From 8220271eee5755e1d0d87f5064bfb00304a307bb Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 8 May 2024 22:26:33 +0300 Subject: [PATCH] feat(access): add proxmox user token (#1159) --- .../virtual_environment_user_token.md | 61 ++++ .../import.sh | 3 + .../resource.tf | 15 + fwprovider/access/resource_user_token.go | 306 ++++++++++++++++++ fwprovider/provider.go | 2 + fwprovider/tests/resource_user_test.go | 128 +++++++- fwprovider/tests/test_environment.go | 24 +- proxmox/access/user_token.go | 93 ++++++ proxmox/access/user_token_types.go | 58 ++++ tools/tools.go | 1 + 10 files changed, 670 insertions(+), 21 deletions(-) create mode 100644 docs/resources/virtual_environment_user_token.md create mode 100644 examples/resources/proxmox_virtual_environment_user_token/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_user_token/resource.tf create mode 100644 fwprovider/access/resource_user_token.go create mode 100644 proxmox/access/user_token.go create mode 100644 proxmox/access/user_token_types.go diff --git a/docs/resources/virtual_environment_user_token.md b/docs/resources/virtual_environment_user_token.md new file mode 100644 index 00000000..1f6678b6 --- /dev/null +++ b/docs/resources/virtual_environment_user_token.md @@ -0,0 +1,61 @@ +--- +layout: page +title: proxmox_virtual_environment_user_token +parent: Resources +subcategory: Virtual Environment +description: |- + User API tokens. +--- + +# Resource: proxmox_virtual_environment_user_token + +User API tokens. + +## Example Usage + +```terraform +# if creating a user token, the user must be created first +resource "proxmox_virtual_environment_user" "user" { + comment = "Managed by Terraform" + email = "user@pve" + enabled = true + expiration_date = "2034-01-01T22:00:00Z" + user_id = "user@pve" +} + +resource "proxmox_virtual_environment_user_token" "user_token" { + comment = "Managed by Terraform" + expiration_date = "2033-01-01T22:00:00Z" + token_name = "tk1" + user_id = proxmox_virtual_environment_user.user.user_id +} +``` + + +## Schema + +### Required + +- `token_name` (String) User-specific token identifier. +- `user_id` (String) User identifier. + +### Optional + +- `comment` (String) Comment for the token. +- `expiration_date` (String) Expiration date for the token. +- `privileges_separation` (Boolean) Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user. + +### Read-Only + +- `id` (String) Unique token identifier with format `!`. +- `value` (String, Sensitive) API token value used for authentication. It is populated only when creating a new token, and can't be retrieved at import. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +#Tokens can be imported using they identifiers in format `user_id!token_name` format, e.g.: +terraform import proxmox_virtual_environment_user_token.token1 user@pve!token1 +``` diff --git a/examples/resources/proxmox_virtual_environment_user_token/import.sh b/examples/resources/proxmox_virtual_environment_user_token/import.sh new file mode 100644 index 00000000..b8a3267d --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_user_token/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +#Tokens can be imported using they identifiers in format `user_id!token_name` format, e.g.: +terraform import proxmox_virtual_environment_user_token.token1 user@pve!token1 diff --git a/examples/resources/proxmox_virtual_environment_user_token/resource.tf b/examples/resources/proxmox_virtual_environment_user_token/resource.tf new file mode 100644 index 00000000..3389f1aa --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_user_token/resource.tf @@ -0,0 +1,15 @@ +# if creating a user token, the user must be created first +resource "proxmox_virtual_environment_user" "user" { + comment = "Managed by Terraform" + email = "user@pve" + enabled = true + expiration_date = "2034-01-01T22:00:00Z" + user_id = "user@pve" +} + +resource "proxmox_virtual_environment_user_token" "user_token" { + comment = "Managed by Terraform" + expiration_date = "2033-01-01T22:00:00Z" + token_name = "tk1" + user_id = proxmox_virtual_environment_user.user.user_id +} diff --git a/fwprovider/access/resource_user_token.go b/fwprovider/access/resource_user_token.go new file mode 100644 index 00000000..331e0e5f --- /dev/null +++ b/fwprovider/access/resource_user_token.go @@ -0,0 +1,306 @@ +package access + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/structure" + "github.com/bpg/terraform-provider-proxmox/fwprovider/validators" + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/access" + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +var ( + _ resource.Resource = &userTokenResource{} + _ resource.ResourceWithConfigure = &userTokenResource{} + _ resource.ResourceWithImportState = &userTokenResource{} +) + +type userTokenResource struct { + client proxmox.Client +} + +type userTokenModel struct { + Comment types.String `tfsdk:"comment"` + ExpirationDate types.String `tfsdk:"expiration_date"` + ID types.String `tfsdk:"id"` + PrivSeparation types.Bool `tfsdk:"privileges_separation"` + UserID types.String `tfsdk:"user_id"` + TokenName types.String `tfsdk:"token_name"` + Value types.String `tfsdk:"value"` +} + +// NewUserTokenResource creates a new user token resource. +func NewUserTokenResource() resource.Resource { + return &userTokenResource{} +} + +func (r *userTokenResource) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "User API tokens.", + Attributes: map[string]schema.Attribute{ + "comment": schema.StringAttribute{ + Description: "Comment for the token.", + Optional: true, + }, + "expiration_date": schema.StringAttribute{ + Description: "Expiration date for the token.", + Optional: true, + Validators: []validator.String{ + validators.NewParseValidator(func(s string) (time.Time, error) { + return time.Parse(time.RFC3339, s) + }, "must be a valid RFC3339 date"), + }, + }, + "id": structure.IDAttribute("Unique token identifier with format `!`."), + "privileges_separation": schema.BoolAttribute{ + Description: "Restrict API token privileges with separate ACLs (default)", + MarkdownDescription: "Restrict API token privileges with separate ACLs (default), " + + "or give full privileges of corresponding user.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "token_name": schema.StringAttribute{ + Description: "User-specific token identifier.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`[A-Za-z][A-Za-z0-9.\-_]+`), "must be a valid token identifier"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "user_id": schema.StringAttribute{ + Description: "User identifier.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "value": schema.StringAttribute{ + Description: "API token value used for authentication.", + MarkdownDescription: "API token value used for authentication. It is populated only when creating a new token, " + + "and can't be retrieved at import.", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (r *userTokenResource) Configure( + _ context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *userTokenResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_user_token" +} + +func (r *userTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan userTokenModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + body := access.UserTokenCreateRequestBody{ + Comment: plan.Comment.ValueStringPointer(), + PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()), + } + + if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" { + expirationDate, err := time.Parse( + time.RFC3339, + plan.ExpirationDate.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error parsing expiration date", err.Error()) + return + } + + v := expirationDate.Unix() + body.ExpirationDate = &v + } + + value, err := r.client.Access().CreateUserToken(ctx, plan.UserID.ValueString(), plan.TokenName.ValueString(), &body) + if err != nil { + resp.Diagnostics.AddError("Error creating user token", err.Error()) + } + + if resp.Diagnostics.HasError() { + return + } + + plan.ID = types.StringValue(plan.UserID.ValueString() + "!" + plan.TokenName.ValueString()) + plan.Value = types.StringValue(value) + resp.State.Set(ctx, plan) +} + +func (r *userTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state userTokenModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + data, err := r.client.Access().GetUserToken(ctx, state.UserID.ValueString(), state.TokenName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error reading user token", err.Error()) + return + } + + state.Comment = types.StringPointerValue(data.Comment) + + if data.ExpirationDate != nil { + dt := time.Unix(int64(*data.ExpirationDate), 0).UTC().Format(time.RFC3339) + state.ExpirationDate = types.StringValue(dt) + } + + state.PrivSeparation = types.BoolPointerValue(data.PrivSeparate.PointerBool()) + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +func (r *userTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state userTokenModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + body := access.UserTokenUpdateRequestBody{ + Comment: plan.Comment.ValueStringPointer(), + PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()), + } + + if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" { + expirationDate, err := time.Parse( + time.RFC3339, + plan.ExpirationDate.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error parsing expiration date", err.Error()) + return + } + + v := expirationDate.Unix() + body.ExpirationDate = &v + } + + err := r.client.Access().UpdateUserToken(ctx, plan.UserID.ValueString(), plan.TokenName.ValueString(), &body) + if err != nil { + resp.Diagnostics.AddError("Error creating user token", err.Error()) + } + + if resp.Diagnostics.HasError() { + return + } + + plan.Value = types.StringNull() + + resp.State.Set(ctx, plan) +} + +func (r *userTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state userTokenModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.Access().DeleteUserToken(ctx, state.UserID.ValueString(), state.TokenName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error deleting user token", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *userTokenResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + idParts := strings.Split(req.ID, "!") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: 'user_id!token_name'. Got: %q", req.ID), + ) + + return + } + + userID := idParts[0] + tokenName := idParts[1] + + data, err := r.client.Access().GetUserToken(ctx, userID, tokenName) + if err != nil { + resp.Diagnostics.AddError("Error reading user token", err.Error()) + return + } + + state := userTokenModel{ + Comment: types.StringPointerValue(data.Comment), + ID: types.StringValue(req.ID), + PrivSeparation: types.BoolPointerValue(data.PrivSeparate.PointerBool()), + UserID: types.StringValue(userID), + TokenName: types.StringValue(tokenName), + Value: types.StringNull(), + } + + if data.ExpirationDate != nil { + state.ExpirationDate = types.StringValue(time.Unix(int64(*data.ExpirationDate), 0).UTC().Format(time.RFC3339)) + } + + diags := resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} diff --git a/fwprovider/provider.go b/fwprovider/provider.go index a3af15b5..355676d7 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/bpg/terraform-provider-proxmox/fwprovider/access" "github.com/bpg/terraform-provider-proxmox/fwprovider/ha" "github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping" "github.com/bpg/terraform-provider-proxmox/fwprovider/network" @@ -446,6 +447,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc hardwaremapping.NewResourceUSB, network.NewLinuxBridgeResource, network.NewLinuxVLANResource, + access.NewUserTokenResource, vm.NewVMResource, NewClusterOptionsResource, NewDownloadFileResource, diff --git a/fwprovider/tests/resource_user_test.go b/fwprovider/tests/resource_user_test.go index fa1a7234..158f2ba7 100644 --- a/fwprovider/tests/resource_user_test.go +++ b/fwprovider/tests/resource_user_test.go @@ -7,9 +7,15 @@ package tests import ( + "context" + "fmt" "testing" + "github.com/brianvoe/gofakeit/v7" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" + + "github.com/bpg/terraform-provider-proxmox/proxmox/access" ) func TestAccResourceUser(t *testing.T) { @@ -17,45 +23,55 @@ func TestAccResourceUser(t *testing.T) { te := initTestEnvironment(t) + username := fmt.Sprintf("%s@pve", gofakeit.Username()) + te.addTemplateVars(map[string]any{ + "Username": username, + }) + tests := []struct { name string steps []resource.TestStep }{ {"create and update user", []resource.TestStep{ { - Config: `resource "proxmox_virtual_environment_user" "user1" { + Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" { comment = "Managed by Terraform" - email = "user1@pve" + email = "{{.Username}}" enabled = true expiration_date = "2034-01-01T22:00:00Z" first_name = "First" last_name = "Last" - user_id = "user1@pve" - }`, - Check: testResourceAttributes("proxmox_virtual_environment_user.user1", map[string]string{ + user_id = "{{.Username}}" + }`), + Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{ "comment": "Managed by Terraform", - "email": "user1@pve", + "email": username, "enabled": "true", "expiration_date": "2034-01-01T22:00:00Z", "first_name": "First", "last_name": "Last", - "user_id": "user1@pve", + "user_id": username, }), }, { - Config: `resource "proxmox_virtual_environment_user" "user1" { + Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" { enabled = false expiration_date = "2035-01-01T22:00:00Z" - user_id = "user1@pve" + user_id = "{{.Username}}" first_name = "First One" - }`, - Check: testResourceAttributes("proxmox_virtual_environment_user.user1", map[string]string{ + }`), + Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{ "enabled": "false", "expiration_date": "2035-01-01T22:00:00Z", "first_name": "First One", - "user_id": "user1@pve", + "user_id": username, }), }, + { + ResourceName: "proxmox_virtual_environment_user.user", + ImportState: true, + ImportStateVerify: true, + }, }}, } @@ -68,3 +84,91 @@ func TestAccResourceUser(t *testing.T) { }) } } + +func TestAccResourceUserToken(t *testing.T) { + t.Parallel() + + te := initTestEnvironment(t) + username := fmt.Sprintf("%s@pve", gofakeit.Username()) + tokenName := gofakeit.Word() + + te.addTemplateVars(map[string]any{ + "Username": username, + "TokenName": tokenName, + }) + + tests := []struct { + name string + preCheck func() + steps []resource.TestStep + }{ + { + "create and update user token", + func() { + err := te.accessClient().CreateUser(context.Background(), &access.UserCreateRequestBody{ + ID: username, + Password: gofakeit.Password(true, true, true, true, false, 8), + }) + require.NoError(t, err) + + t.Cleanup(func() { + err := te.accessClient().DeleteUser(context.Background(), username) + require.NoError(t, err) + }) + }, + []resource.TestStep{ + { + Config: te.renderConfig(`resource "proxmox_virtual_environment_user_token" "user_token" { + comment = "Managed by Terraform" + expiration_date = "2034-01-01T22:00:00Z" + token_name = "{{.TokenName}}" + user_id = "{{.Username}}" + }`), + Check: testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{ + "comment": "Managed by Terraform", + "expiration_date": "2034-01-01T22:00:00Z", + "id": fmt.Sprintf("%s!%s", username, tokenName), + "user_id": username, + "value": fmt.Sprintf("%s!%s=.*", username, tokenName), + }), + }, + { + Config: te.renderConfig(`resource "proxmox_virtual_environment_user_token" "user_token" { + comment = "Managed by Terraform 2" + expiration_date = "2033-01-01T01:01:01Z" + privileges_separation = false + token_name = "{{.TokenName}}" + user_id = "{{.Username}}" + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{ + "comment": "Managed by Terraform 2", + "expiration_date": "2033-01-01T01:01:01Z", + "privileges_separation": "false", + "token_name": tokenName, + "user_id": username, + }), + testNoResourceAttributesSet("proxmox_virtual_environment_user_token.user_token", []string{ + "value", + }), + ), + }, + { + ResourceName: "proxmox_virtual_environment_user_token.user_token", + ImportState: true, + ImportStateVerify: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: te.accProviders, + PreCheck: tt.preCheck, + Steps: tt.steps, + }) + }) + } +} diff --git a/fwprovider/tests/test_environment.go b/fwprovider/tests/test_environment.go index 4694ccef..f3412f3a 100644 --- a/fwprovider/tests/test_environment.go +++ b/fwprovider/tests/test_environment.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/stretchr/testify/require" + "github.com/bpg/terraform-provider-proxmox/proxmox/access" sdkV2provider "github.com/bpg/terraform-provider-proxmox/proxmoxtf/provider" "github.com/bpg/terraform-provider-proxmox/fwprovider" @@ -35,7 +36,7 @@ type testEnvironment struct { accProviders map[string]func() (tfprotov6.ProviderServer, error) once sync.Once - nc *nodes.Client + c api.Client } func initTestEnvironment(t *testing.T) *testEnvironment { @@ -109,8 +110,8 @@ func (e *testEnvironment) renderConfig(cfg string) string { return buf.String() } -func (e *testEnvironment) nodeClient() *nodes.Client { - if e.nc == nil { +func (e *testEnvironment) client() api.Client { + if e.c == nil { e.once.Do( func() { username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME") @@ -128,21 +129,26 @@ func (e *testEnvironment) nodeClient() *nodes.Client { panic(err) } - client, err := api.NewClient(creds, conn) + e.c, err = api.NewClient(creds, conn) if err != nil { panic(err) } - - e.nc = &nodes.Client{Client: client, NodeName: e.nodeName} }) } - return e.nc + return e.c +} + +func (e *testEnvironment) accessClient() *access.Client { + return &access.Client{Client: e.client()} +} + +func (e *testEnvironment) nodeClient() *nodes.Client { + return &nodes.Client{Client: e.client(), NodeName: e.nodeName} } func (e *testEnvironment) nodeStorageClient() *storage.Client { - nodesClient := e.nodeClient() - return &storage.Client{Client: nodesClient, StorageName: e.datastoreID} + return &storage.Client{Client: e.nodeClient(), StorageName: e.datastoreID} } // testAccMuxProviders returns a map of mux servers for the acceptance tests. diff --git a/proxmox/access/user_token.go b/proxmox/access/user_token.go new file mode 100644 index 00000000..22ab79d7 --- /dev/null +++ b/proxmox/access/user_token.go @@ -0,0 +1,93 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package access + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +func (c *Client) userTokensPath(id string) string { + return fmt.Sprintf("%s/%s/token", c.usersPath(), url.PathEscape(id)) +} + +func (c *Client) userTokenPath(userid, id string) string { + return fmt.Sprintf("%s/%s", c.userTokensPath(userid), url.PathEscape(id)) +} + +// CreateUserToken creates a user token. +func (c *Client) CreateUserToken( + ctx context.Context, + userid string, + id string, + d *UserTokenCreateRequestBody, +) (string, error) { + resBody := &UserTokenCreateResponseBody{} + + err := c.DoRequest(ctx, http.MethodPost, c.userTokenPath(userid, id), d, resBody) + if err != nil { + return "", fmt.Errorf("error creating user token: %w", err) + } + + return resBody.Data.FullTokenID + "=" + resBody.Data.Value, nil +} + +// DeleteUserToken deletes an user token. +func (c *Client) DeleteUserToken(ctx context.Context, userid string, id string) error { + err := c.DoRequest(ctx, http.MethodDelete, c.userTokenPath(userid, id), nil, nil) + if err != nil { + return fmt.Errorf("error deleting user token: %w", err) + } + + return nil +} + +// GetUserToken retrieves a user token. +func (c *Client) GetUserToken(ctx context.Context, userid string, id string) (*UserTokenGetResponseData, error) { + resBody := &UserTokenGetResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.userTokenPath(userid, id), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error retrieving user token: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + +// ListUserTokens retrieves a list of user tokens. +func (c *Client) ListUserTokens(ctx context.Context, userid string) ([]*UserTokenListResponseData, error) { + resBody := &UserTokenListResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.userTokensPath(userid), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error listing user tokens: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + +// UpdateUserToken updates the user token. +func (c *Client) UpdateUserToken(ctx context.Context, userid string, id string, d *UserTokenUpdateRequestBody) error { + err := c.DoRequest(ctx, http.MethodPut, c.userTokenPath(userid, id), d, nil) + if err != nil { + return fmt.Errorf("error updating user token: %w", err) + } + + return nil +} diff --git a/proxmox/access/user_token_types.go b/proxmox/access/user_token_types.go new file mode 100644 index 00000000..c8fd2440 --- /dev/null +++ b/proxmox/access/user_token_types.go @@ -0,0 +1,58 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package access + +import ( + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// UserTokenCreateRequestBody contains the data for a user token create request. +type UserTokenCreateRequestBody struct { + Comment *string `json:"comment,omitempty" url:"comment,omitempty"` + ExpirationDate *int64 `json:"expire,omitempty" url:"expire,omitempty"` + PrivSeparate *types.CustomBool `json:"privsep,omitempty" url:"privsep,omitempty,int"` +} + +// UserTokenUpdateRequestBody contains the data for a user token update request. +type UserTokenUpdateRequestBody UserTokenCreateRequestBody + +// UserTokenCreateResponseBody contains the body from a user token create response. +type UserTokenCreateResponseBody struct { + Data *UserTokenCreateResponseData `json:"data,omitempty"` +} + +// UserTokenCreateResponseData contains the data from a user token create response. +type UserTokenCreateResponseData struct { + // The full token id, format "!" + FullTokenID string `json:"full-tokenid"` + Info UserTokenGetResponseData `json:"info"` + Value string `json:"value"` +} + +// UserTokenGetResponseBody contains the body from a user token get response. +type UserTokenGetResponseBody struct { + Data *UserTokenGetResponseData `json:"data,omitempty"` +} + +// UserTokenGetResponseData contains the data from a user token get response. +type UserTokenGetResponseData struct { + Comment *string `json:"comment,omitempty" url:"comment,omitempty"` + PrivSeparate *types.CustomBool `json:"privsep,omitempty" url:"privsep,omitempty,int"` + ExpirationDate *types.CustomInt64 `json:"expire,omitempty" url:"expire,omitempty"` +} + +// UserTokenListResponseBody contains the body from a user token list response. +type UserTokenListResponseBody struct { + Data []*UserTokenListResponseData `json:"data,omitempty"` +} + +// UserTokenListResponseData contains the data from a user token list response. +type UserTokenListResponseData struct { + UserTokenGetResponseData + + TokenID string `json:"tokenid"` +} diff --git a/tools/tools.go b/tools/tools.go index 038ab499..5c15c686 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -45,4 +45,5 @@ import ( //go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/ +//go:generate cp ../build/docs-gen/resources/virtual_environment_user_token.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_vm2.md ../docs/resources/