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
path: /tmp/gotest.log
- uses: hashicorp/setup-terraform@v3
- name: Check for uncommitted changes in generated docs
run: make docs && git diff --exit-code

1
.gitignore vendored
View File

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

View File

@ -15,10 +15,12 @@ Use the navigation to the left to read about the available resources.
```hcl
provider "proxmox" {
endpoint = "https://10.0.0.2:8006/"
# TODO: use terraform variable or remove the line, and use PROXMOX_VE_USERNAME environment variable
username = "root@pam"
# TODO: use terraform variable or remove the line, and use PROXMOX_VE_PASSWORD environment variable
password = "the-password-set-during-installation-of-proxmox-ve"
# because self-signed TLS certificate is in use
insecure = true
# uncomment (unless on Windows...)
@ -35,15 +37,20 @@ provider "proxmox" {
## 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.
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
provider "proxmox" {
endpoint = "https://10.0.0.2:8006/"
username = "username@realm"
password = "a-strong-password"
}
@ -54,6 +61,7 @@ A better approach is to extract these values into Terraform variables, and refer
```hcl
provider "proxmox" {
endpoint = var.virtual_environment_endpoint
username = var.virtual_environment_username
password = var.virtual_environment_password
}
@ -75,12 +83,54 @@ provider "proxmox" {
```sh
export PROXMOX_VE_USERNAME="username@realm"
export PROXMOX_VE_PASSWORD="a-strong-password"
export PROXMOX_VE_PASSWORD='a-strong-password'
terraform plan
```
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
~> 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.
You can provide the private key from a file:
```hcl
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.
Note that the content of the private key is injected using `<<-` format to ignore indentation:
```hcl
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.
- `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`.
- `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`).
- `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`.
- `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.
- `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`.

View File

@ -10,7 +10,6 @@ import (
"context"
"fmt"
"net"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
@ -60,14 +59,17 @@ type proxmoxProvider struct {
// proxmoxProviderModel maps provider schema data.
type proxmoxProviderModel struct {
APIToken types.String `tfsdk:"api_token"`
Endpoint types.String `tfsdk:"endpoint"`
Insecure types.Bool `tfsdk:"insecure"`
MinTLS types.String `tfsdk:"min_tls"`
OTP types.String `tfsdk:"otp"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
SSH []struct {
Endpoint types.String `tfsdk:"endpoint"`
Insecure types.Bool `tfsdk:"insecure"`
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"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
SSH []struct {
Agent types.Bool `tfsdk:"agent"`
AgentSocket types.String `tfsdk:"agent_socket"`
PrivateKey types.String `tfsdk:"private_key"`
@ -100,12 +102,16 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Description: "The API token for the Proxmox VE API.",
Optional: 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{
Description: "The endpoint for the Proxmox VE API.",
@ -134,14 +140,14 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
Optional: true,
Sensitive: true,
},
"username": schema.StringAttribute{
Description: "The username for the Proxmox VE API.",
Optional: true,
},
"tmp_dir": schema.StringAttribute{
Description: "The alternative temporary directory.",
Optional: true,
},
"username": schema.StringAttribute{
Description: "The username for the Proxmox VE API.",
Optional: true,
},
},
Blocks: map[string]schema.Block{
// 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.",
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{
Description: "The password used for the SSH connection. " +
"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,
Sensitive: true,
},
"username": schema.StringAttribute{
Description: "The username used for the SSH connection. " +
"Defaults to the value of the `username` field of the " +
"`provider` block.",
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,
},
"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{
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.",
Optional: 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,
"username": schema.StringAttribute{
Description: "The username used for the SSH connection. " +
"Defaults to the value of the `username` field of the " +
"`provider` block.",
Optional: true,
},
},
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.",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the Proxmox VE node.",
Required: true,
},
"address": schema.StringAttribute{
Description: "The address of the Proxmox VE node.",
Required: true,
},
"name": schema.StringAttribute{
Description: "The name of the Proxmox VE node.",
Required: true,
},
"port": schema.Int64Attribute{
Description: "The port of the Proxmox VE node.",
Optional: true,
@ -265,17 +271,15 @@ func (p *proxmoxProvider) Configure(
// with Terraform configuration value if set.
// Check environment variables
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE")
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")
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
if !config.APIToken.IsNull() {
apiToken = config.APIToken.ValueString()
}
if !config.Endpoint.IsNull() {
endpoint = config.Endpoint.ValueString()
}
@ -288,6 +292,18 @@ func (p *proxmoxProvider) Configure(
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() {
username = config.Username.ValueString()
}
@ -312,7 +328,7 @@ func (p *proxmoxProvider) Configure(
// 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 {
resp.Diagnostics.AddError(
"Unable to create Proxmox VE API credentials",
@ -401,12 +417,12 @@ func (p *proxmoxProvider) Configure(
}
}
if sshUsername == "" {
sshUsername = strings.Split(creds.Username, "@")[0]
if sshUsername == "" && creds.UserCredentials != nil {
sshUsername = strings.Split(creds.UserCredentials.Username, "@")[0]
}
if sshPassword == "" {
sshPassword = creds.Password
if sshPassword == "" && creds.UserCredentials != nil {
sshPassword = creds.UserCredentials.Password
}
sshClient, err := ssh.NewClient(

View File

@ -144,12 +144,14 @@ func (e *Environment) Client() api.Client {
if e.c == nil {
e.once.Do(
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")
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 {
panic(err)
}

View File

@ -115,11 +115,7 @@ type client struct {
}
// NewClient creates and initializes a VirtualEnvironmentClient instance.
func NewClient(creds *Credentials, conn *Connection) (Client, error) {
if creds == nil {
return nil, errors.New("credentials must not be nil")
}
func NewClient(creds Credentials, conn *Connection) (Client, error) {
if conn == 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
if creds.APIToken != nil {
auth, err = NewTokenAuthenticator(*creds.APIToken)
} else {
auth, err = NewTicketAuthenticator(conn, creds)
switch {
case creds.TokenCredentials != nil:
auth, err = NewTokenAuthenticator(*creds.TokenCredentials)
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 {
return nil, err
return nil, fmt.Errorf("failed to create API client: %w", err)
}
return &client{

View File

@ -8,54 +8,95 @@ package api
import (
"errors"
"regexp"
"strings"
)
const rootUsername = "root@pam"
// Credentials is a struct that holds the credentials for the Proxmox Virtual
// Environment API.
// Credentials contains the credentials for authenticating with the Proxmox VE API.
type Credentials struct {
Username string
Password string
OTP *string
APIToken *string
UserCredentials *UserCredentials
TokenCredentials *TokenCredentials
TicketCredentials *TicketCredentials
}
// NewCredentials creates a new Credentials struct.
func NewCredentials(username, password, otp, apiToken string) (*Credentials, error) {
if apiToken != "" {
return &Credentials{
APIToken: &apiToken,
}, nil
// UserCredentials contains the username, password, and OTP for authenticating with the Proxmox VE API.
type UserCredentials struct {
Username string
Password string
OTP string
}
// TokenCredentials contains the API token for authenticating with the Proxmox VE API.
type TokenCredentials struct {
APIToken string
}
// TicketCredentials contains the auth ticket and CSRF prevention token for authenticating with the Proxmox VE API.
type TicketCredentials struct {
AuthTicket string
CSRFPreventionToken string
}
// NewCredentials creates a new set of credentials for authenticating with the Proxmox VE API.
// The order of precedence is:
// 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
}
if password == "" {
return nil, errors.New(
"you must specify a password for the Proxmox Virtual Environment API",
)
tic, err := newTicketCredentials(authTicket, csrfPreventionToken)
if err == nil {
return Credentials{TicketCredentials: &tic}, nil
}
if username == "" {
return nil, errors.New(
"you must specify a username for the Proxmox Virtual Environment API",
)
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, "@") {
return nil, errors.New(
"make sure the username for the Proxmox Virtual Environment API ends in '@pve or @pam'",
)
return UserCredentials{}, errors.New("username must end with '@pve' or '@pam'")
}
c := &Credentials{
return UserCredentials{
Username: username,
Password: password,
}
if otp != "" {
c.OTP = &otp
}
return c, nil
OTP: otp,
}, nil
}
func newTokenCredentials(apiToken string) (TokenCredentials, error) {
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 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
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"
"strings"
)
type ticketAuthenticator struct {
conn *Connection
authRequest string
authData *AuthenticationResponseData
mu sync.Mutex
authData *AuthenticationResponseData
}
// NewTicketAuthenticator returns a new ticket authenticator.
func NewTicketAuthenticator(conn *Connection, creds *Credentials) (Authenticator, error) {
authRequest := fmt.Sprintf(
"username=%s&password=%s",
url.QueryEscape(creds.Username),
url.QueryEscape(creds.Password),
)
func NewTicketAuthenticator(creds TicketCredentials) (Authenticator, error) {
ard := &AuthenticationResponseData{}
ard.Ticket = &(creds.AuthTicket)
ard.CSRFPreventionToken = &(creds.CSRFPreventionToken)
// OTP is optional, and probably doesn't make much sense for most provider users.
if creds.OTP != nil {
authRequest = fmt.Sprintf("%s&otp=%s", authRequest, url.QueryEscape(*creds.OTP))
authTicketSplits := strings.Split(creds.AuthTicket, ":")
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{
conn: conn,
authRequest: authRequest,
authData: ard,
}, 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 {
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.
func (t *ticketAuthenticator) AuthenticateRequest(ctx context.Context, req *http.Request) error {
a, err := t.authenticate(ctx)
if err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
func (t *ticketAuthenticator) AuthenticateRequest(_ context.Context, req *http.Request) error {
req.AddCookie(&http.Cookie{
HttpOnly: true,
Name: "PVEAuthCookie",
Secure: true,
Value: *a.Ticket,
Value: *t.authData.Ticket,
})
if req.Method != http.MethodGet {
req.Header.Add("CSRFPreventionToken", *a.CSRFPreventionToken)
req.Header.Add("CSRFPreventionToken", *t.authData.CSRFPreventionToken)
}
return nil

View File

@ -19,7 +19,9 @@ type AuthenticationResponseBody struct {
type AuthenticationResponseCapabilities struct {
Access *types.CustomPrivileges `json:"access,omitempty"`
Datacenter *types.CustomPrivileges `json:"dc,omitempty"`
Mapping *types.CustomPrivileges `json:"mapping,omitempty"`
Nodes *types.CustomPrivileges `json:"nodes,omitempty"`
SDN *types.CustomPrivileges `json:"sdn,omitempty"`
Storage *types.CustomPrivileges `json:"storage,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
// for authentication.
func NewTokenAuthenticator(token string) (Authenticator, error) {
func NewTokenAuthenticator(toc TokenCredentials) (Authenticator, error) {
return &tokenAuthenticator{
username: strings.Split(token, "!")[0],
token: token,
username: strings.Split(toc.APIToken, "!")[0],
token: toc.APIToken,
}, 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 creds *api.Credentials
var creds api.Credentials
var conn *api.Connection
// Check environment variables
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN", "PM_VE_API_TOKEN")
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT", "PM_VE_ENDPOINT")
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE", "PM_VE_INSECURE")
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")
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 {
endpoint = v.(string)
@ -71,6 +69,22 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
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 {
username = v.(string)
}
@ -79,11 +93,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
password = v.(string)
}
if v, ok := d.GetOk(mkProviderOTP); ok {
otp = v.(string)
}
creds, err = api.NewCredentials(username, password, otp, apiToken)
creds, err = api.NewCredentials(username, password, otp, apiToken, authTicket, csrfPreventionToken)
diags = append(diags, diag.FromErr(err)...)
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")
if v, ok := sshConf[mkProviderSSHUsername]; !ok || v.(string) == "" {
if sshUsername != "" {
switch {
case sshUsername != "":
sshConf[mkProviderSSHUsername] = sshUsername
} else {
sshConf[mkProviderSSHUsername] = strings.Split(creds.Username, "@")[0]
case creds.UserCredentials != nil:
sshConf[mkProviderSSHUsername] = strings.Split(creds.UserCredentials.Username, "@")[0]
default:
sshConf[mkProviderSSHUsername] = ""
}
}
if v, ok := sshConf[mkProviderSSHPassword]; !ok || v.(string) == "" {
if sshPassword != "" {
switch {
case sshPassword != "":
sshConf[mkProviderSSHPassword] = sshPassword
} else {
sshConf[mkProviderSSHPassword] = creds.Password
case creds.UserCredentials != nil:
sshConf[mkProviderSSHPassword] = creds.UserCredentials.Password
default:
sshConf[mkProviderSSHPassword] = ""
}
}

View File

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

View File

@ -8,30 +8,31 @@ package provider
import (
"os"
"regexp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
const (
mkProviderEndpoint = "endpoint"
mkProviderInsecure = "insecure"
mkProviderMinTLS = "min_tls"
mkProviderOTP = "otp"
mkProviderPassword = "password"
mkProviderUsername = "username"
mkProviderAPIToken = "api_token"
mkProviderTmpDir = "tmp_dir"
mkProviderSSH = "ssh"
mkProviderSSHUsername = "username"
mkProviderSSHPassword = "password"
mkProviderSSHAgent = "agent"
mkProviderSSHAgentSocket = "agent_socket"
mkProviderSSHPrivateKey = "private_key"
mkProviderSSHSocks5Server = "socks5_server"
mkProviderSSHSocks5Username = "socks5_username"
mkProviderSSHSocks5Password = "socks5_password"
mkProviderEndpoint = "endpoint"
mkProviderInsecure = "insecure"
mkProviderMinTLS = "min_tls"
mkProviderAuthTicket = "auth_ticket"
mkProviderCSRFPreventionToken = "csrf_prevention_token" // #nosec G101
mkProviderAPIToken = "api_token"
mkProviderOTP = "otp"
mkProviderPassword = "password"
mkProviderUsername = "username"
mkProviderTmpDir = "tmp_dir"
mkProviderSSH = "ssh"
mkProviderSSHUsername = "username"
mkProviderSSHPassword = "password"
mkProviderSSHAgent = "agent"
mkProviderSSHAgentSocket = "agent_socket"
mkProviderSSHPrivateKey = "private_key"
mkProviderSSHSocks5Server = "socks5_server"
mkProviderSSHSocks5Username = "socks5_username"
mkProviderSSHSocks5Password = "socks5_password"
mkProviderSSHNode = "node"
mkProviderSSHNodeName = "name"
@ -58,6 +59,27 @@ func createSchema() map[string]*schema.Schema {
Description: "The minimum required TLS version for API calls." +
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
},
mkProviderAuthTicket: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The pre-authenticated Ticket for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderCSRFPreventionToken: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The pre-authenticated CSRF Prevention Token for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderAPIToken: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The API token for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderOTP: {
Type: schema.TypeString,
Optional: true,
@ -65,28 +87,18 @@ func createSchema() map[string]*schema.Schema {
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,
Optional: true,
Sensitive: true,
Description: "The password for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderUsername: {
Type: schema.TypeString,
Optional: true,
Description: "The username for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderAPIToken: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The API token for the Proxmox VE API.",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(
regexp.MustCompile(`^\S+@\S+!\S+=([a-zA-Z0-9-]+)$`),
"Must be a valid API token, e.g. 'USER@REALM!TOKENID=UUID'",
)),
mkProviderPassword: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The password for the Proxmox VE API.",
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderSSH: {
Type: schema.TypeList,

View File

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

View File

@ -23,7 +23,7 @@ import (
// to ensure the documentation is formatted properly.
//go:generate terraform fmt -recursive ../examples/
// 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
// for the resources / data sources that have been migrated.