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:
parent
bf2d2dc396
commit
eb2f36be21
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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
1
.gitignore
vendored
@ -19,6 +19,7 @@ modules-dev/
|
|||||||
.run/
|
.run/
|
||||||
|
|
||||||
*~
|
*~
|
||||||
|
*#
|
||||||
*.backup
|
*.backup
|
||||||
*.bak
|
*.bak
|
||||||
*.dll
|
*.dll
|
||||||
|
@ -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`.
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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{
|
||||||
|
@ -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 {
|
||||||
Username string
|
UserCredentials *UserCredentials
|
||||||
Password string
|
TokenCredentials *TokenCredentials
|
||||||
OTP *string
|
TicketCredentials *TicketCredentials
|
||||||
APIToken *string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCredentials creates a new Credentials struct.
|
// UserCredentials contains the username, password, and OTP for authenticating with the Proxmox VE API.
|
||||||
func NewCredentials(username, password, otp, apiToken string) (*Credentials, error) {
|
type UserCredentials struct {
|
||||||
if apiToken != "" {
|
Username string
|
||||||
return &Credentials{
|
Password string
|
||||||
APIToken: &apiToken,
|
OTP string
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
// 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 == "" {
|
tic, err := newTicketCredentials(authTicket, csrfPreventionToken)
|
||||||
return nil, errors.New(
|
if err == nil {
|
||||||
"you must specify a password for the Proxmox Virtual Environment API",
|
return Credentials{TicketCredentials: &tic}, nil
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if username == "" {
|
usr, err := newUserCredentials(username, password, otp)
|
||||||
return nil, errors.New(
|
if err == nil {
|
||||||
"you must specify a username for the Proxmox Virtual Environment API",
|
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 != "" {
|
}
|
||||||
c.OTP = &otp
|
|
||||||
}
|
func newTokenCredentials(apiToken string) (TokenCredentials, error) {
|
||||||
|
re := regexp.MustCompile(`^\S+@\S+!\S+=([a-zA-Z0-9-]+)$`)
|
||||||
return c, nil
|
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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
@ -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
151
proxmox/api/user_auth.go
Normal 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
|
||||||
|
}
|
@ -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] = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user