0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-08-22 19:38:35 +00:00

feat(provider): add support for pre(external) auth'd session tokens (#1441)

* feat(provider): add support for pre(external) auth'd session tokens

adds provider config inputs:
  - env vars: PROXMOX_VE_AUTH_PAYLOAD; PROXMOX_VE_AUTH_TICKET with PROXMOX_VE_CSRF_PREVENTION_TOKEN
  - provider-config: auth_payload; auth_ticket with csrf_prevention_token

Signed-off-by: vanillaSprinkles <vanillaSprinkles@users.noreply.github.com>

* add //nolint to "todo" comments/questions and lll for build to pass; add flags to terraform-plugin-docs

Signed-off-by: vanillaSprinkles <vanillaSprinkles@users.noreply.github.com>

* address first iteration of comments: remove auth-payload, improve index.md

Signed-off-by: vanillaSprinkles <vanillaSprinkles@users.noreply.github.com>

* refactor credentials using struct composition, other minor cleanups

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

* fix linter error

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

* fix make docs, add terraform to handle fmt

Signed-off-by: vanillaSprinkles <vanillaSprinkles@users.noreply.github.com>

---------

Signed-off-by: vanillaSprinkles <vanillaSprinkles@users.noreply.github.com>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
vanillaSprinkles 2024-10-03 00:40:33 +00:00 committed by GitHub
parent bf2d2dc396
commit eb2f36be21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 493 additions and 267 deletions

View File

@ -88,5 +88,7 @@ jobs:
name: test-log name: test-log
path: /tmp/gotest.log path: /tmp/gotest.log
- uses: hashicorp/setup-terraform@v3
- name: Check for uncommitted changes in generated docs - name: Check for uncommitted changes in generated docs
run: make docs && git diff --exit-code run: make docs && git diff --exit-code

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ modules-dev/
.run/ .run/
*~ *~
*#
*.backup *.backup
*.bak *.bak
*.dll *.dll

View File

@ -15,10 +15,12 @@ Use the navigation to the left to read about the available resources.
```hcl ```hcl
provider "proxmox" { provider "proxmox" {
endpoint = "https://10.0.0.2:8006/" endpoint = "https://10.0.0.2:8006/"
# TODO: use terraform variable or remove the line, and use PROXMOX_VE_USERNAME environment variable # TODO: use terraform variable or remove the line, and use PROXMOX_VE_USERNAME environment variable
username = "root@pam" username = "root@pam"
# TODO: use terraform variable or remove the line, and use PROXMOX_VE_PASSWORD environment variable # TODO: use terraform variable or remove the line, and use PROXMOX_VE_PASSWORD environment variable
password = "the-password-set-during-installation-of-proxmox-ve" password = "the-password-set-during-installation-of-proxmox-ve"
# because self-signed TLS certificate is in use # because self-signed TLS certificate is in use
insecure = true insecure = true
# uncomment (unless on Windows...) # uncomment (unless on Windows...)
@ -35,15 +37,20 @@ provider "proxmox" {
## Authentication ## Authentication
The Proxmox provider offers a flexible means of providing credentials for authentication. The Proxmox provider offers a flexible means of providing credentials for authentication.
Static credentials can be provided to the `proxmox` block through either a `api_token` or a combination of `username` and `password` arguments. Static credentials and pre-authenticated session-ticket can be provided to the `proxmox` block through one the choices of arguments below, ordered by precedence:
- `auth_ticket` and `csrf_prevention_token`
- `api_token`
- `username` and `password`
!> Hard-coding credentials into any Terraform configuration is not recommended, and risks secret leakage should this file ever be committed to a public version control system. !> Hard-coding credentials into any Terraform configuration is not recommended, and risks secret leakage should this file ever be committed to a public version control system.
Static credentials can be provided by adding a `username` and `password`, or `api_token` in-line in the Proxmox provider block: Static credentials can be provided in-line in the Proxmox provider block, by adding one of the arguments above (example with username and password):
```hcl ```hcl
provider "proxmox" { provider "proxmox" {
endpoint = "https://10.0.0.2:8006/" endpoint = "https://10.0.0.2:8006/"
username = "username@realm" username = "username@realm"
password = "a-strong-password" password = "a-strong-password"
} }
@ -54,6 +61,7 @@ A better approach is to extract these values into Terraform variables, and refer
```hcl ```hcl
provider "proxmox" { provider "proxmox" {
endpoint = var.virtual_environment_endpoint endpoint = var.virtual_environment_endpoint
username = var.virtual_environment_username username = var.virtual_environment_username
password = var.virtual_environment_password password = var.virtual_environment_password
} }
@ -75,12 +83,54 @@ provider "proxmox" {
```sh ```sh
export PROXMOX_VE_USERNAME="username@realm" export PROXMOX_VE_USERNAME="username@realm"
export PROXMOX_VE_PASSWORD="a-strong-password" export PROXMOX_VE_PASSWORD='a-strong-password'
terraform plan terraform plan
``` ```
See the [Argument Reference](#argument-reference) section for the supported variable names and use cases. See the [Argument Reference](#argument-reference) section for the supported variable names and use cases.
## Pre-Authentication, or Passing an Authentication Ticket into the provider
It is possible to generate a session ticket with the API, and to pass the ticket and csrf_prevention_token into the provider using environment variables `PROXMOX_VE_AUTH_TICKET` and `PROXMOX_VE_CSRF_PREVENTION_TOKEN` (or provider's arguments `auth_ticket` and `csrf_prevention_token`).
An example of using `curl` and `jq` to query the Proxmox API to get a Proxmox session ticket; it is also very easy to pass in a TOTP password this way:
```hcl
provider "proxmox" {
endpoint = "https://10.0.0.2:8006/"
}
```
```bash
#!/usr/bin/bash
## assume vars are set: PROXMOX_VE_ENDPOINT, PROXMOX_VE_USERNAME, PROXMOX_VE_PASSWORD
## end-goal: automatically set PROXMOX_VE_AUTH_TICKET and PROXMOX_VE_CSRF_PREVENTION_TOKEN
_user_totp_password='123456' ## optional TOTP password
proxmox_api_ticket_path='api2/json/access/ticket' ## cannot have double "//" - ensure endpoint ends with a "/" and this string does not begin with a "/", or vice-versa
## call the auth api endpoint
resp=$( curl -q -s -k --data-urlencode "username=${PROXMOX_VE_USERNAME}" --data-urlencode "password=${PROXMOX_VE_PASSWORD}" "${PROXMOX_VE_ENDPOINT}${proxmox_api_ticket_path}" )
auth_ticket=$( jq -r '.data.ticket' <<<"${resp}" )
resp_csrf=$( jq -r '.data.CSRFPreventionToken' <<<"${resp}" )
## check if the response payload needs a TFA (totp) passed, call the auth-api endpoint again
if [[ $(jq -r '.data.NeedTFA' <<<"${resp}") == 1 ]]; then
resp=$( curl -q -s -k -H "CSRFPreventionToken: ${resp_csrf}" --data-urlencode "username=${PROXMOX_VE_USERNAME}" --data-urlencode "tfa-challenge=${auth_ticket}" --data-urlencode "password=totp:${_user_totp_password}" "${PROXMOX_VE_ENDPOINT}${proxmox_api_ticket_path}" )
auth_ticket=$( jq -r '.data.ticket' <<<"${resp}" )
resp_csrf=$( jq -r '.data.CSRFPreventionToken' <<<"${resp}" )
fi
export PROXMOX_VE_AUTH_TICKET="${auth_ticket}"
export PROXMOX_VE_CSRF_PREVENTION_TOKEN="${resp_csrf}"
terraform plan
```
## SSH Connection ## SSH Connection
~> Please read if you are using VMs with custom disk images, or uploading snippets. ~> Please read if you are using VMs with custom disk images, or uploading snippets.
@ -126,6 +176,7 @@ you can use the `private_key` argument in the `ssh` block (or alternatively `PRO
The private key mut not be encrypted, and must be in PEM format. The private key mut not be encrypted, and must be in PEM format.
You can provide the private key from a file: You can provide the private key from a file:
```hcl ```hcl
provider "proxmox" { provider "proxmox" {
// ... // ...
@ -138,6 +189,7 @@ provider "proxmox" {
Alternatively, although not recommended due to the increased risk of exposing an unprotected key, heredoc syntax can be used to supply the private key as a string. Alternatively, although not recommended due to the increased risk of exposing an unprotected key, heredoc syntax can be used to supply the private key as a string.
Note that the content of the private key is injected using `<<-` format to ignore indentation: Note that the content of the private key is injected using `<<-` format to ignore indentation:
```hcl ```hcl
provider "proxmox" { provider "proxmox" {
// ... // ...
@ -349,10 +401,17 @@ In addition to [generic provider arguments](https://www.terraform.io/docs/config
- `endpoint` - (Required) The endpoint for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_ENDPOINT`). Usually this is `https://<your-cluster-endpoint>:8006/`. **Do not** include `/api2/json` at the end. - `endpoint` - (Required) The endpoint for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_ENDPOINT`). Usually this is `https://<your-cluster-endpoint>:8006/`. **Do not** include `/api2/json` at the end.
- `insecure` - (Optional) Whether to skip the TLS verification step (can also be sourced from `PROXMOX_VE_INSECURE`). If omitted, defaults to `false`. - `insecure` - (Optional) Whether to skip the TLS verification step (can also be sourced from `PROXMOX_VE_INSECURE`). If omitted, defaults to `false`.
- `min_tls` - (Optional) The minimum required TLS version for API calls (can also be sourced from `PROXMOX_VE_MIN_TLS`). Supported values: `1.0|1.1|1.2|1.3`. If omitted, defaults to `1.3`. - `min_tls` - (Optional) The minimum required TLS version for API calls (can also be sourced from `PROXMOX_VE_MIN_TLS`). Supported values: `1.0|1.1|1.2|1.3`. If omitted, defaults to `1.3`.
- `auth_ticket` - (Optional) The auth ticket from an external auth call (can also be sourced from `PROXMOX_VE_AUTH_TICKET`). To be used in conjunction with `csrf_prevention_token`, takes precedence over `api_token` and `username` with `password`. For example, `PVE:username@realm:12345678::some_base64_payload==`.
- `csrf_prevention_token` - (Optional) The CSRF Prevention Token from an external auth call (can also be sourced from `PROXMOX_VE_CSRF_PREVENTION_TOKEN`). For example, `12345678:some_blob`.
- `api_token` - (Optional) The API Token for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_API_TOKEN`). Takes precedence over `username` with `password`. For example, `username@realm!for-terraform-provider=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
- `otp` - (Optional, Deprecated) The one-time password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_OTP`). - `otp` - (Optional, Deprecated) The one-time password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_OTP`).
- `password` - (Required) The password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_PASSWORD`).
- `username` - (Required) The username and realm for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For example, `root@pam`. - `username` - (Required) The username and realm for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For example, `root@pam`.
- `api_token` - (Optional) The API Token for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_API_TOKEN`). For example, `root@pam!for-terraform-provider=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. - `password` - (Required) The password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_PASSWORD`).
- `ssh` - (Optional) The SSH connection configuration to a Proxmox node. This is a block, whose fields are documented below. - `ssh` - (Optional) The SSH connection configuration to a Proxmox node. This is a block, whose fields are documented below.
- `username` - (Optional) The username to use for the SSH connection. Defaults to the username used for the Proxmox API connection. Can also be sourced from `PROXMOX_VE_SSH_USERNAME`. Required when using API Token. - `username` - (Optional) The username to use for the SSH connection. Defaults to the username used for the Proxmox API connection. Can also be sourced from `PROXMOX_VE_SSH_USERNAME`. Required when using API Token.
- `password` - (Optional) The password to use for the SSH connection. Defaults to the password used for the Proxmox API connection. Can also be sourced from `PROXMOX_VE_SSH_PASSWORD`. - `password` - (Optional) The password to use for the SSH connection. Defaults to the password used for the Proxmox API connection. Can also be sourced from `PROXMOX_VE_SSH_PASSWORD`.

View File

@ -10,7 +10,6 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"regexp"
"strings" "strings"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
@ -60,13 +59,16 @@ type proxmoxProvider struct {
// proxmoxProviderModel maps provider schema data. // proxmoxProviderModel maps provider schema data.
type proxmoxProviderModel struct { type proxmoxProviderModel struct {
APIToken types.String `tfsdk:"api_token"`
Endpoint types.String `tfsdk:"endpoint"` Endpoint types.String `tfsdk:"endpoint"`
Insecure types.Bool `tfsdk:"insecure"` Insecure types.Bool `tfsdk:"insecure"`
MinTLS types.String `tfsdk:"min_tls"` MinTLS types.String `tfsdk:"min_tls"`
AuthTicket types.String `tfsdk:"auth_ticket"`
CSRFPreventionToken types.String `tfsdk:"csrf_prevention_token"`
APIToken types.String `tfsdk:"api_token"`
OTP types.String `tfsdk:"otp"` OTP types.String `tfsdk:"otp"`
Username types.String `tfsdk:"username"` Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"` Password types.String `tfsdk:"password"`
SSH []struct { SSH []struct {
Agent types.Bool `tfsdk:"agent"` Agent types.Bool `tfsdk:"agent"`
AgentSocket types.String `tfsdk:"agent_socket"` AgentSocket types.String `tfsdk:"agent_socket"`
@ -100,12 +102,16 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Description: "The API token for the Proxmox VE API.", Description: "The API token for the Proxmox VE API.",
Optional: true, Optional: true,
Sensitive: true, Sensitive: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^\S+@\S+!\S+=([a-zA-Z0-9-]+)$`),
`must be a valid API token, e.g. 'USER@REALM!TOKENID=UUID'`,
),
}, },
"auth_ticket": schema.StringAttribute{
Description: "The pre-authenticated Ticket for the Proxmox VE API.",
Optional: true,
Sensitive: true,
},
"csrf_prevention_token": schema.StringAttribute{
Description: "The pre-authenticated CSRF Prevention Token for the Proxmox VE API.",
Optional: true,
Sensitive: true,
}, },
"endpoint": schema.StringAttribute{ "endpoint": schema.StringAttribute{
Description: "The endpoint for the Proxmox VE API.", Description: "The endpoint for the Proxmox VE API.",
@ -134,14 +140,14 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Optional: true, Optional: true,
Sensitive: true, Sensitive: true,
}, },
"username": schema.StringAttribute{
Description: "The username for the Proxmox VE API.",
Optional: true,
},
"tmp_dir": schema.StringAttribute{ "tmp_dir": schema.StringAttribute{
Description: "The alternative temporary directory.", Description: "The alternative temporary directory.",
Optional: true, Optional: true,
}, },
"username": schema.StringAttribute{
Description: "The username for the Proxmox VE API.",
Optional: true,
},
}, },
Blocks: map[string]schema.Block{ Blocks: map[string]schema.Block{
// have to define it as a list due to backwards compatibility // have to define it as a list due to backwards compatibility
@ -164,12 +170,6 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
"environment variable.", "environment variable.",
Optional: true, Optional: true,
}, },
"private_key": schema.StringAttribute{
Description: "The unencrypted private key (in PEM format) used for the SSH connection. " +
"Defaults to the value of the `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable.",
Optional: true,
Sensitive: true,
},
"password": schema.StringAttribute{ "password": schema.StringAttribute{
Description: "The password used for the SSH connection. " + Description: "The password used for the SSH connection. " +
"Defaults to the value of the `password` field of the " + "Defaults to the value of the `password` field of the " +
@ -177,11 +177,17 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Optional: true, Optional: true,
Sensitive: true, Sensitive: true,
}, },
"username": schema.StringAttribute{ "private_key": schema.StringAttribute{
Description: "The username used for the SSH connection. " + Description: "The unencrypted private key (in PEM format) used for the SSH connection. " +
"Defaults to the value of the `username` field of the " + "Defaults to the value of the `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable.",
"`provider` block.",
Optional: true, Optional: true,
Sensitive: true,
},
"socks5_password": schema.StringAttribute{
Description: "The password for the SOCKS5 proxy server. " +
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_PASSWORD` environment variable.",
Optional: true,
Sensitive: true,
}, },
"socks5_server": schema.StringAttribute{ "socks5_server": schema.StringAttribute{
Description: "The address:port of the SOCKS5 proxy server. " + Description: "The address:port of the SOCKS5 proxy server. " +
@ -193,11 +199,11 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_USERNAME` environment variable.", "Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_USERNAME` environment variable.",
Optional: true, Optional: true,
}, },
"socks5_password": schema.StringAttribute{ "username": schema.StringAttribute{
Description: "The password for the SOCKS5 proxy server. " + Description: "The username used for the SSH connection. " +
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_PASSWORD` environment variable.", "Defaults to the value of the `username` field of the " +
"`provider` block.",
Optional: true, Optional: true,
Sensitive: true,
}, },
}, },
Blocks: map[string]schema.Block{ Blocks: map[string]schema.Block{
@ -205,14 +211,14 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Description: "Overrides for SSH connection configuration for a Proxmox VE node.", Description: "Overrides for SSH connection configuration for a Proxmox VE node.",
NestedObject: schema.NestedBlockObject{ NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{ Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the Proxmox VE node.",
Required: true,
},
"address": schema.StringAttribute{ "address": schema.StringAttribute{
Description: "The address of the Proxmox VE node.", Description: "The address of the Proxmox VE node.",
Required: true, Required: true,
}, },
"name": schema.StringAttribute{
Description: "The name of the Proxmox VE node.",
Required: true,
},
"port": schema.Int64Attribute{ "port": schema.Int64Attribute{
Description: "The port of the Proxmox VE node.", Description: "The port of the Proxmox VE node.",
Optional: true, Optional: true,
@ -265,17 +271,15 @@ func (p *proxmoxProvider) Configure(
// with Terraform configuration value if set. // with Terraform configuration value if set.
// Check environment variables // Check environment variables
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT") endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE") insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE")
minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS") minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS")
authTicket := utils.GetAnyStringEnv("PROXMOX_VE_AUTH_TICKET")
csrfPreventionToken := utils.GetAnyStringEnv("PROXMOX_VE_CSRF_PREVENTION_TOKEN")
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME") username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD") password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
if !config.APIToken.IsNull() {
apiToken = config.APIToken.ValueString()
}
if !config.Endpoint.IsNull() { if !config.Endpoint.IsNull() {
endpoint = config.Endpoint.ValueString() endpoint = config.Endpoint.ValueString()
} }
@ -288,6 +292,18 @@ func (p *proxmoxProvider) Configure(
minTLS = config.MinTLS.ValueString() minTLS = config.MinTLS.ValueString()
} }
if !config.AuthTicket.IsNull() {
authTicket = config.AuthTicket.ValueString()
}
if !config.CSRFPreventionToken.IsNull() {
csrfPreventionToken = config.CSRFPreventionToken.ValueString()
}
if !config.APIToken.IsNull() {
apiToken = config.APIToken.ValueString()
}
if !config.Username.IsNull() { if !config.Username.IsNull() {
username = config.Username.ValueString() username = config.Username.ValueString()
} }
@ -312,7 +328,7 @@ func (p *proxmoxProvider) Configure(
// Create the Proxmox VE API client // Create the Proxmox VE API client
creds, err := api.NewCredentials(username, password, "", apiToken) creds, err := api.NewCredentials(username, password, "", apiToken, authTicket, csrfPreventionToken)
if err != nil { if err != nil {
resp.Diagnostics.AddError( resp.Diagnostics.AddError(
"Unable to create Proxmox VE API credentials", "Unable to create Proxmox VE API credentials",
@ -401,12 +417,12 @@ func (p *proxmoxProvider) Configure(
} }
} }
if sshUsername == "" { if sshUsername == "" && creds.UserCredentials != nil {
sshUsername = strings.Split(creds.Username, "@")[0] sshUsername = strings.Split(creds.UserCredentials.Username, "@")[0]
} }
if sshPassword == "" { if sshPassword == "" && creds.UserCredentials != nil {
sshPassword = creds.Password sshPassword = creds.UserCredentials.Password
} }
sshClient, err := ssh.NewClient( sshClient, err := ssh.NewClient(

View File

@ -144,12 +144,14 @@ func (e *Environment) Client() api.Client {
if e.c == nil { if e.c == nil {
e.once.Do( e.once.Do(
func() { func() {
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
authTicket := utils.GetAnyStringEnv("PROXMOX_VE_AUTH_TICKET")
csrfPreventionToken := utils.GetAnyStringEnv("PROXMOX_VE_CSRF_PREVENTION_TOKEN")
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME") username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD") password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
creds, err := api.NewCredentials(username, password, "", apiToken) creds, err := api.NewCredentials(username, password, "", apiToken, authTicket, csrfPreventionToken)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -115,11 +115,7 @@ type client struct {
} }
// NewClient creates and initializes a VirtualEnvironmentClient instance. // NewClient creates and initializes a VirtualEnvironmentClient instance.
func NewClient(creds *Credentials, conn *Connection) (Client, error) { func NewClient(creds Credentials, conn *Connection) (Client, error) {
if creds == nil {
return nil, errors.New("credentials must not be nil")
}
if conn == nil { if conn == nil {
return nil, errors.New("connection must not be nil") return nil, errors.New("connection must not be nil")
} }
@ -128,14 +124,19 @@ func NewClient(creds *Credentials, conn *Connection) (Client, error) {
var err error var err error
if creds.APIToken != nil { switch {
auth, err = NewTokenAuthenticator(*creds.APIToken) case creds.TokenCredentials != nil:
} else { auth, err = NewTokenAuthenticator(*creds.TokenCredentials)
auth, err = NewTicketAuthenticator(conn, creds) case creds.TicketCredentials != nil:
auth, err = NewTicketAuthenticator(*creds.TicketCredentials)
case creds.UserCredentials != nil:
auth = NewUserAuthenticator(*creds.UserCredentials, conn)
default:
return nil, errors.New("must provide either user credentials, an API token, or a ticket")
} }
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create API client: %w", err)
} }
return &client{ return &client{

View File

@ -8,54 +8,95 @@ package api
import ( import (
"errors" "errors"
"regexp"
"strings" "strings"
) )
const rootUsername = "root@pam" const rootUsername = "root@pam"
// Credentials is a struct that holds the credentials for the Proxmox Virtual // Credentials contains the credentials for authenticating with the Proxmox VE API.
// Environment API.
type Credentials struct { type Credentials struct {
UserCredentials *UserCredentials
TokenCredentials *TokenCredentials
TicketCredentials *TicketCredentials
}
// UserCredentials contains the username, password, and OTP for authenticating with the Proxmox VE API.
type UserCredentials struct {
Username string Username string
Password string Password string
OTP *string OTP string
APIToken *string
} }
// NewCredentials creates a new Credentials struct. // TokenCredentials contains the API token for authenticating with the Proxmox VE API.
func NewCredentials(username, password, otp, apiToken string) (*Credentials, error) { type TokenCredentials struct {
if apiToken != "" { APIToken string
return &Credentials{
APIToken: &apiToken,
}, nil
} }
if password == "" { // TicketCredentials contains the auth ticket and CSRF prevention token for authenticating with the Proxmox VE API.
return nil, errors.New( type TicketCredentials struct {
"you must specify a password for the Proxmox Virtual Environment API", AuthTicket string
) CSRFPreventionToken string
} }
if username == "" { // NewCredentials creates a new set of credentials for authenticating with the Proxmox VE API.
return nil, errors.New( // The order of precedence is:
"you must specify a username for the Proxmox Virtual Environment API", // 1. API token
) // 2. Ticket
// 3. User credentials.
func NewCredentials(username, password, otp, apiToken, authTicket, csrfPreventionToken string) (Credentials, error) {
tok, err := newTokenCredentials(apiToken)
if err == nil {
return Credentials{TokenCredentials: &tok}, nil
}
tic, err := newTicketCredentials(authTicket, csrfPreventionToken)
if err == nil {
return Credentials{TicketCredentials: &tic}, nil
}
usr, err := newUserCredentials(username, password, otp)
if err == nil {
return Credentials{UserCredentials: &usr}, nil
}
return Credentials{}, errors.New("must provide either user credentials, an API token, or a ticket")
}
func newUserCredentials(username, password, otp string) (UserCredentials, error) {
if username == "" || password == "" {
return UserCredentials{}, errors.New("both username and password are required")
} }
if !strings.Contains(username, "@") { if !strings.Contains(username, "@") {
return nil, errors.New( return UserCredentials{}, errors.New("username must end with '@pve' or '@pam'")
"make sure the username for the Proxmox Virtual Environment API ends in '@pve or @pam'",
)
} }
c := &Credentials{ return UserCredentials{
Username: username, Username: username,
Password: password, Password: password,
OTP: otp,
}, nil
} }
if otp != "" { func newTokenCredentials(apiToken string) (TokenCredentials, error) {
c.OTP = &otp re := regexp.MustCompile(`^\S+@\S+!\S+=([a-zA-Z0-9-]+)$`)
if !re.MatchString(apiToken) {
return TokenCredentials{}, errors.New("must be a valid API token, e.g. 'USER@REALM!TOKENID=UUID'")
} }
return c, nil return TokenCredentials{
APIToken: apiToken,
}, nil
}
func newTicketCredentials(authTicket, csrfPreventionToken string) (TicketCredentials, error) {
if authTicket == "" || csrfPreventionToken == "" {
return TicketCredentials{}, errors.New("both authTicket and csrfPreventionToken are required")
}
return TicketCredentials{
AuthTicket: authTicket,
CSRFPreventionToken: csrfPreventionToken,
}, nil
} }

View File

@ -7,114 +7,39 @@
package api package api
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/url" "strings"
"sync"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/utils"
) )
type ticketAuthenticator struct { type ticketAuthenticator struct {
conn *Connection
authRequest string
authData *AuthenticationResponseData authData *AuthenticationResponseData
mu sync.Mutex
} }
// NewTicketAuthenticator returns a new ticket authenticator. // NewTicketAuthenticator returns a new ticket authenticator.
func NewTicketAuthenticator(conn *Connection, creds *Credentials) (Authenticator, error) { func NewTicketAuthenticator(creds TicketCredentials) (Authenticator, error) {
authRequest := fmt.Sprintf( ard := &AuthenticationResponseData{}
"username=%s&password=%s", ard.Ticket = &(creds.AuthTicket)
url.QueryEscape(creds.Username), ard.CSRFPreventionToken = &(creds.CSRFPreventionToken)
url.QueryEscape(creds.Password),
)
// OTP is optional, and probably doesn't make much sense for most provider users. authTicketSplits := strings.Split(creds.AuthTicket, ":")
if creds.OTP != nil {
authRequest = fmt.Sprintf("%s&otp=%s", authRequest, url.QueryEscape(*creds.OTP)) if len(authTicketSplits) > 3 {
ard.Username = strings.Split(creds.AuthTicket, ":")[1]
} else {
return nil, errors.New("AuthTicket must include a valid username")
}
if !strings.Contains(ard.Username, "@") {
return nil, errors.New("username must end with '@pve' or '@pam'")
} }
return &ticketAuthenticator{ return &ticketAuthenticator{
conn: conn, authData: ard,
authRequest: authRequest,
}, nil }, nil
} }
func (t *ticketAuthenticator) authenticate(ctx context.Context) (*AuthenticationResponseData, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.authData != nil {
return t.authData, nil
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/%s/access/ticket", t.conn.endpoint, basePathJSONAPI),
bytes.NewBufferString(t.authRequest),
)
if err != nil {
return nil, fmt.Errorf("failed to create authentication request: %w", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
tflog.Debug(ctx, "Sending authentication request", map[string]interface{}{
"path": req.URL.Path,
})
//nolint:bodyclose
res, err := t.conn.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to retrieve authentication response: %w", err)
}
defer utils.CloseOrLogError(ctx)(res.Body)
err = validateResponseCode(res)
if err != nil {
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
resBody := AuthenticationResponseBody{}
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication response, %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the authentication response")
}
if resBody.Data.CSRFPreventionToken == nil {
return nil, errors.New(
"the server did not include a CSRF prevention token in the authentication response",
)
}
if resBody.Data.Ticket == nil {
return nil, errors.New("the server did not include a ticket in the authentication response")
}
if resBody.Data.Username == "" {
return nil, errors.New("the server did not include the username in the authentication response")
}
t.authData = resBody.Data
return resBody.Data, nil
}
func (t *ticketAuthenticator) IsRoot() bool { func (t *ticketAuthenticator) IsRoot() bool {
return t.authData != nil && t.authData.Username == rootUsername return t.authData != nil && t.authData.Username == rootUsername
} }
@ -124,21 +49,16 @@ func (t *ticketAuthenticator) IsRootTicket() bool {
} }
// AuthenticateRequest adds authentication data to a new request. // AuthenticateRequest adds authentication data to a new request.
func (t *ticketAuthenticator) AuthenticateRequest(ctx context.Context, req *http.Request) error { func (t *ticketAuthenticator) AuthenticateRequest(_ context.Context, req *http.Request) error {
a, err := t.authenticate(ctx)
if err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
req.AddCookie(&http.Cookie{ req.AddCookie(&http.Cookie{
HttpOnly: true, HttpOnly: true,
Name: "PVEAuthCookie", Name: "PVEAuthCookie",
Secure: true, Secure: true,
Value: *a.Ticket, Value: *t.authData.Ticket,
}) })
if req.Method != http.MethodGet { if req.Method != http.MethodGet {
req.Header.Add("CSRFPreventionToken", *a.CSRFPreventionToken) req.Header.Add("CSRFPreventionToken", *t.authData.CSRFPreventionToken)
} }
return nil return nil

View File

@ -19,7 +19,9 @@ type AuthenticationResponseBody struct {
type AuthenticationResponseCapabilities struct { type AuthenticationResponseCapabilities struct {
Access *types.CustomPrivileges `json:"access,omitempty"` Access *types.CustomPrivileges `json:"access,omitempty"`
Datacenter *types.CustomPrivileges `json:"dc,omitempty"` Datacenter *types.CustomPrivileges `json:"dc,omitempty"`
Mapping *types.CustomPrivileges `json:"mapping,omitempty"`
Nodes *types.CustomPrivileges `json:"nodes,omitempty"` Nodes *types.CustomPrivileges `json:"nodes,omitempty"`
SDN *types.CustomPrivileges `json:"sdn,omitempty"`
Storage *types.CustomPrivileges `json:"storage,omitempty"` Storage *types.CustomPrivileges `json:"storage,omitempty"`
VMs *types.CustomPrivileges `json:"vms,omitempty"` VMs *types.CustomPrivileges `json:"vms,omitempty"`
} }

View File

@ -19,10 +19,10 @@ type tokenAuthenticator struct {
// NewTokenAuthenticator creates a new authenticator that uses a PVE API Token // NewTokenAuthenticator creates a new authenticator that uses a PVE API Token
// for authentication. // for authentication.
func NewTokenAuthenticator(token string) (Authenticator, error) { func NewTokenAuthenticator(toc TokenCredentials) (Authenticator, error) {
return &tokenAuthenticator{ return &tokenAuthenticator{
username: strings.Split(token, "!")[0], username: strings.Split(toc.APIToken, "!")[0],
token: token, token: toc.APIToken,
}, nil }, nil
} }

151
proxmox/api/user_auth.go Normal file
View File

@ -0,0 +1,151 @@
/*
* 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 api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sync"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/utils"
)
type userAuthenticator struct {
conn *Connection
authRequest string
authData *AuthenticationResponseData
mu sync.Mutex
}
// NewUserAuthenticator creates a new authenticator that uses a username and password for authentication.
func NewUserAuthenticator(creds UserCredentials, conn *Connection) Authenticator {
authRequest := fmt.Sprintf(
"username=%s&password=%s",
url.QueryEscape(creds.Username),
url.QueryEscape(creds.Password),
)
// OTP is optional, and probably doesn't make much sense for most provider users.
// TOTP uses 2x requests; one with payloads `username=` and `password=`,
// (this returns a payload including: 'NeedTFA=1')
// followed by a 2nd request with payloads:
// `username=`, `tfa-challenge=<firsts response ticket>`, `password=totp:######`,
// and header: `CSRFPreventionToken: <first response CSRF>`
// Ticket generated lasts for ~2hours (to verify)
if creds.OTP != "" {
authRequest = fmt.Sprintf("%s&otp=%s", authRequest, url.QueryEscape(creds.OTP))
}
return &userAuthenticator{
conn: conn,
authRequest: authRequest,
}
}
func (t *userAuthenticator) authenticate(ctx context.Context) (*AuthenticationResponseData, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.authData != nil {
return t.authData, nil
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/%s/access/ticket", t.conn.endpoint, basePathJSONAPI),
bytes.NewBufferString(t.authRequest),
)
if err != nil {
return nil, fmt.Errorf("failed to create authentication request: %w", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
tflog.Debug(ctx, "Sending authentication request", map[string]interface{}{
"path": req.URL.Path,
})
//nolint:bodyclose
res, err := t.conn.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to retrieve authentication response: %w", err)
}
defer utils.CloseOrLogError(ctx)(res.Body)
err = validateResponseCode(res)
if err != nil {
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
resBody := AuthenticationResponseBody{}
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication response, %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the authentication response")
}
if resBody.Data.CSRFPreventionToken == nil {
return nil, errors.New(
"the server did not include a CSRF prevention token in the authentication response",
)
}
if resBody.Data.Ticket == nil {
return nil, errors.New("the server did not include a ticket in the authentication response")
}
if resBody.Data.Username == "" {
return nil, errors.New("the server did not include the username in the authentication response")
}
t.authData = resBody.Data
return resBody.Data, nil
}
func (t *userAuthenticator) IsRoot() bool {
return t.authData != nil && t.authData.Username == rootUsername
}
func (t *userAuthenticator) IsRootTicket() bool {
return t.IsRoot()
}
// AuthenticateRequest adds authentication data to a new request.
func (t *userAuthenticator) AuthenticateRequest(ctx context.Context, req *http.Request) error {
a, err := t.authenticate(ctx)
if err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
req.AddCookie(&http.Cookie{
HttpOnly: true,
Name: "PVEAuthCookie",
Secure: true,
Value: *a.Ticket,
})
if req.Method != http.MethodGet {
req.Header.Add("CSRFPreventionToken", *a.CSRFPreventionToken)
}
return nil
}

View File

@ -42,22 +42,20 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
var sshClient ssh.Client var sshClient ssh.Client
var creds *api.Credentials var creds api.Credentials
var conn *api.Connection var conn *api.Connection
// Check environment variables // Check environment variables
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN", "PM_VE_API_TOKEN")
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT", "PM_VE_ENDPOINT") endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT", "PM_VE_ENDPOINT")
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE", "PM_VE_INSECURE") insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE", "PM_VE_INSECURE")
minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS", "PM_VE_MIN_TLS") minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS", "PM_VE_MIN_TLS")
authTicket := utils.GetAnyStringEnv("PROXMOX_VE_AUTH_TICKET", "PM_VE_AUTH_TICKET")
csrfPreventionToken := utils.GetAnyStringEnv("PROXMOX_VE_CSRF_PREVENTION_TOKEN", "PM_VE_CSRF_PREVENTION_TOKEN")
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN", "PM_VE_API_TOKEN")
otp := utils.GetAnyStringEnv("PROXMOX_VE_OTP", "PM_VE_OTP")
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME", "PM_VE_USERNAME") username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME", "PM_VE_USERNAME")
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD", "PM_VE_PASSWORD") password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD", "PM_VE_PASSWORD")
otp := utils.GetAnyStringEnv("PROXMOX_VE_OTP", "PM_VE_OTP")
if v, ok := d.GetOk(mkProviderAPIToken); ok {
apiToken = v.(string)
}
if v, ok := d.GetOk(mkProviderEndpoint); ok { if v, ok := d.GetOk(mkProviderEndpoint); ok {
endpoint = v.(string) endpoint = v.(string)
@ -71,6 +69,22 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
minTLS = v.(string) minTLS = v.(string)
} }
if v, ok := d.GetOk(mkProviderAuthTicket); ok {
authTicket = v.(string)
}
if v, ok := d.GetOk(mkProviderCSRFPreventionToken); ok {
csrfPreventionToken = v.(string)
}
if v, ok := d.GetOk(mkProviderAPIToken); ok {
apiToken = v.(string)
}
if v, ok := d.GetOk(mkProviderOTP); ok {
otp = v.(string)
}
if v, ok := d.GetOk(mkProviderUsername); ok { if v, ok := d.GetOk(mkProviderUsername); ok {
username = v.(string) username = v.(string)
} }
@ -79,11 +93,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
password = v.(string) password = v.(string)
} }
if v, ok := d.GetOk(mkProviderOTP); ok { creds, err = api.NewCredentials(username, password, otp, apiToken, authTicket, csrfPreventionToken)
otp = v.(string)
}
creds, err = api.NewCredentials(username, password, otp, apiToken)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
conn, err = api.NewConnection(endpoint, insecure, minTLS) conn, err = api.NewConnection(endpoint, insecure, minTLS)
@ -117,18 +127,24 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
sshSocks5Password := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_PASSWORD") sshSocks5Password := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_PASSWORD")
if v, ok := sshConf[mkProviderSSHUsername]; !ok || v.(string) == "" { if v, ok := sshConf[mkProviderSSHUsername]; !ok || v.(string) == "" {
if sshUsername != "" { switch {
case sshUsername != "":
sshConf[mkProviderSSHUsername] = sshUsername sshConf[mkProviderSSHUsername] = sshUsername
} else { case creds.UserCredentials != nil:
sshConf[mkProviderSSHUsername] = strings.Split(creds.Username, "@")[0] sshConf[mkProviderSSHUsername] = strings.Split(creds.UserCredentials.Username, "@")[0]
default:
sshConf[mkProviderSSHUsername] = ""
} }
} }
if v, ok := sshConf[mkProviderSSHPassword]; !ok || v.(string) == "" { if v, ok := sshConf[mkProviderSSHPassword]; !ok || v.(string) == "" {
if sshPassword != "" { switch {
case sshPassword != "":
sshConf[mkProviderSSHPassword] = sshPassword sshConf[mkProviderSSHPassword] = sshPassword
} else { case creds.UserCredentials != nil:
sshConf[mkProviderSSHPassword] = creds.Password sshConf[mkProviderSSHPassword] = creds.UserCredentials.Password
default:
sshConf[mkProviderSSHPassword] = ""
} }
} }

View File

@ -31,21 +31,25 @@ func TestProviderSchema(t *testing.T) {
s := ProxmoxVirtualEnvironment().Schema s := ProxmoxVirtualEnvironment().Schema
test.AssertOptionalArguments(t, s, []string{ test.AssertOptionalArguments(t, s, []string{
mkProviderUsername,
mkProviderPassword,
mkProviderEndpoint, mkProviderEndpoint,
mkProviderInsecure, mkProviderInsecure,
mkProviderMinTLS, mkProviderMinTLS,
mkProviderAuthTicket,
mkProviderCSRFPreventionToken,
mkProviderOTP, mkProviderOTP,
mkProviderUsername,
mkProviderPassword,
}) })
test.AssertValueTypes(t, s, map[string]schema.ValueType{ test.AssertValueTypes(t, s, map[string]schema.ValueType{
mkProviderUsername: schema.TypeString,
mkProviderPassword: schema.TypeString,
mkProviderEndpoint: schema.TypeString, mkProviderEndpoint: schema.TypeString,
mkProviderInsecure: schema.TypeBool, mkProviderInsecure: schema.TypeBool,
mkProviderMinTLS: schema.TypeString, mkProviderMinTLS: schema.TypeString,
mkProviderAuthTicket: schema.TypeString,
mkProviderCSRFPreventionToken: schema.TypeString,
mkProviderOTP: schema.TypeString, mkProviderOTP: schema.TypeString,
mkProviderUsername: schema.TypeString,
mkProviderPassword: schema.TypeString,
}) })
providerSSHSchema := test.AssertNestedSchemaExistence(t, s, mkProviderSSH) providerSSHSchema := test.AssertNestedSchemaExistence(t, s, mkProviderSSH)

View File

@ -8,7 +8,6 @@ package provider
import ( import (
"os" "os"
"regexp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@ -18,10 +17,12 @@ const (
mkProviderEndpoint = "endpoint" mkProviderEndpoint = "endpoint"
mkProviderInsecure = "insecure" mkProviderInsecure = "insecure"
mkProviderMinTLS = "min_tls" mkProviderMinTLS = "min_tls"
mkProviderAuthTicket = "auth_ticket"
mkProviderCSRFPreventionToken = "csrf_prevention_token" // #nosec G101
mkProviderAPIToken = "api_token"
mkProviderOTP = "otp" mkProviderOTP = "otp"
mkProviderPassword = "password" mkProviderPassword = "password"
mkProviderUsername = "username" mkProviderUsername = "username"
mkProviderAPIToken = "api_token"
mkProviderTmpDir = "tmp_dir" mkProviderTmpDir = "tmp_dir"
mkProviderSSH = "ssh" mkProviderSSH = "ssh"
mkProviderSSHUsername = "username" mkProviderSSHUsername = "username"
@ -58,24 +59,18 @@ func createSchema() map[string]*schema.Schema {
Description: "The minimum required TLS version for API calls." + Description: "The minimum required TLS version for API calls." +
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.", "Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
}, },
mkProviderOTP: { mkProviderAuthTicket: {
Type: schema.TypeString,
Optional: true,
Description: "The one-time password for the Proxmox VE API.",
Deprecated: "The `otp` attribute is deprecated and will be removed in a future release. " +
"Please use the `api_token` attribute instead.",
},
mkProviderPassword: {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Sensitive: true, Sensitive: true,
Description: "The password for the Proxmox VE API.", Description: "The pre-authenticated Ticket for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty, ValidateFunc: validation.StringIsNotEmpty,
}, },
mkProviderUsername: { mkProviderCSRFPreventionToken: {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Description: "The username for the Proxmox VE API.", Sensitive: true,
Description: "The pre-authenticated CSRF Prevention Token for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty, ValidateFunc: validation.StringIsNotEmpty,
}, },
mkProviderAPIToken: { mkProviderAPIToken: {
@ -83,10 +78,27 @@ func createSchema() map[string]*schema.Schema {
Optional: true, Optional: true,
Sensitive: true, Sensitive: true,
Description: "The API token for the Proxmox VE API.", Description: "The API token for the Proxmox VE API.",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch( ValidateFunc: validation.StringIsNotEmpty,
regexp.MustCompile(`^\S+@\S+!\S+=([a-zA-Z0-9-]+)$`), },
"Must be a valid API token, e.g. 'USER@REALM!TOKENID=UUID'", mkProviderOTP: {
)), Type: schema.TypeString,
Optional: true,
Description: "The one-time password for the Proxmox VE API.",
Deprecated: "The `otp` attribute is deprecated and will be removed in a future release. " +
"Please use the `api_token` attribute instead.",
},
mkProviderUsername: {
Type: schema.TypeString,
Optional: true,
Description: "The username for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderPassword: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The password for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
}, },
mkProviderSSH: { mkProviderSSH: {
Type: schema.TypeList, Type: schema.TypeList,

View File

@ -886,7 +886,6 @@ func readURL(
if httpLastModified != "" { if httpLastModified != "" {
var timeParsed time.Time var timeParsed time.Time
timeParsed, err = time.Parse(time.RFC1123, httpLastModified) timeParsed, err = time.Parse(time.RFC1123, httpLastModified)
if err != nil { if err != nil {
timeParsed, err = time.Parse(time.RFC1123Z, httpLastModified) timeParsed, err = time.Parse(time.RFC1123Z, httpLastModified)
if err != nil { if err != nil {

View File

@ -23,7 +23,7 @@ import (
// to ensure the documentation is formatted properly. // to ensure the documentation is formatted properly.
//go:generate terraform fmt -recursive ../examples/ //go:generate terraform fmt -recursive ../examples/
// Generate documentation. // Generate documentation.
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-dir ../ --rendered-website-dir ./build/docs-gen //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-dir ../ --rendered-website-dir ./build/docs-gen --provider-name "terraform-provider-proxmox" --rendered-provider-name "terraform-provider-proxmox" //nolint:lll
// Temporary: while migrating to the TF framework, we need to copy the generated docs to the right place // Temporary: while migrating to the TF framework, we need to copy the generated docs to the right place
// for the resources / data sources that have been migrated. // for the resources / data sources that have been migrated.