From eb2f36be21bc8a74bbd7e74bc7a7f3f6ed2d8daf Mon Sep 17 00:00:00 2001 From: vanillaSprinkles Date: Thu, 3 Oct 2024 00:40:33 +0000 Subject: [PATCH] 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 * add //nolint to "todo" comments/questions and lll for build to pass; add flags to terraform-plugin-docs Signed-off-by: vanillaSprinkles * address first iteration of comments: remove auth-payload, improve index.md Signed-off-by: vanillaSprinkles * 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 --------- Signed-off-by: vanillaSprinkles Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .github/workflows/test.yml | 2 + .gitignore | 1 + docs/index.md | 77 ++++++++++++-- fwprovider/provider.go | 114 ++++++++++++--------- fwprovider/test/test_environment.go | 8 +- proxmox/api/client.go | 21 ++-- proxmox/api/credentials.go | 103 +++++++++++++------ proxmox/api/ticket_auth.go | 120 ++++------------------ proxmox/api/ticket_auth_types.go | 2 + proxmox/api/token_auth.go | 6 +- proxmox/api/user_auth.go | 151 ++++++++++++++++++++++++++++ proxmoxtf/provider/provider.go | 52 ++++++---- proxmoxtf/provider/provider_test.go | 20 ++-- proxmoxtf/provider/schema.go | 80 ++++++++------- proxmoxtf/resource/file.go | 1 - tools/tools.go | 2 +- 16 files changed, 493 insertions(+), 267 deletions(-) create mode 100644 proxmox/api/user_auth.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 173c2437..0047e270 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore index 8104ab5d..4ade4187 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ modules-dev/ .run/ *~ +*# *.backup *.bak *.dll diff --git a/docs/index.md b/docs/index.md index ee16fd8f..5ae410d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. @@ -120,12 +170,13 @@ The SSH agent authentication takes precedence over the `private_key` and `passwo ### SSH Private Key -In some cases where SSH agent is not available, for example when using a CI/CD pipeline that does not support SSH agent forwarding, +In some cases where SSH agent is not available, for example when using a CI/CD pipeline that does not support SSH agent forwarding, you can use the `private_key` argument in the `ssh` block (or alternatively `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable) to provide the private key for the SSH connection. 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" { // ... @@ -273,7 +325,7 @@ provider "proxmox" { } ``` -If enabled, this method will be used for all SSH connections to the target nodes in the cluster. +If enabled, this method will be used for all SSH connections to the target nodes in the cluster. ## API Token Authentication @@ -328,8 +380,8 @@ provider "proxmox" { -> The token authentication is taking precedence over the password authentication. -> Not all Proxmox API operations are supported via API Token. -You may see errors like -`error creating container: received an HTTP 403 response - Reason: Permission check failed (changing feature flags for privileged container is only allowed for root@pam)` or +You may see errors like +`error creating container: received an HTTP 403 response - Reason: Permission check failed (changing feature flags for privileged container is only allowed for root@pam)` or `error creating VM: received an HTTP 500 response - Reason: only root can set 'arch' config` or `Permission check failed (user != root@pam)` when using API Token authentication, even when `Administrator` role or the `root@pam` user is used with the token. The workaround is to use password authentication for those operations. @@ -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://: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`. diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 10b0b5d7..8397208e 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -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( diff --git a/fwprovider/test/test_environment.go b/fwprovider/test/test_environment.go index 18fc48ca..30aca243 100644 --- a/fwprovider/test/test_environment.go +++ b/fwprovider/test/test_environment.go @@ -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) } diff --git a/proxmox/api/client.go b/proxmox/api/client.go index 7151188f..10fc38e4 100644 --- a/proxmox/api/client.go +++ b/proxmox/api/client.go @@ -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{ diff --git a/proxmox/api/credentials.go b/proxmox/api/credentials.go index fab5e822..f59334a0 100644 --- a/proxmox/api/credentials.go +++ b/proxmox/api/credentials.go @@ -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 } diff --git a/proxmox/api/ticket_auth.go b/proxmox/api/ticket_auth.go index 642a6eab..dcb812bf 100644 --- a/proxmox/api/ticket_auth.go +++ b/proxmox/api/ticket_auth.go @@ -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 diff --git a/proxmox/api/ticket_auth_types.go b/proxmox/api/ticket_auth_types.go index 5419c746..722c1b45 100644 --- a/proxmox/api/ticket_auth_types.go +++ b/proxmox/api/ticket_auth_types.go @@ -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"` } diff --git a/proxmox/api/token_auth.go b/proxmox/api/token_auth.go index beccbd8b..f5c5c0b3 100644 --- a/proxmox/api/token_auth.go +++ b/proxmox/api/token_auth.go @@ -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 } diff --git a/proxmox/api/user_auth.go b/proxmox/api/user_auth.go new file mode 100644 index 00000000..5c112c32 --- /dev/null +++ b/proxmox/api/user_auth.go @@ -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=`, `password=totp:######`, + // and header: `CSRFPreventionToken: ` + // 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 +} diff --git a/proxmoxtf/provider/provider.go b/proxmoxtf/provider/provider.go index 74bf3527..a7d8518c 100644 --- a/proxmoxtf/provider/provider.go +++ b/proxmoxtf/provider/provider.go @@ -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] = "" } } diff --git a/proxmoxtf/provider/provider_test.go b/proxmoxtf/provider/provider_test.go index 786c45bf..d8918eb8 100644 --- a/proxmoxtf/provider/provider_test.go +++ b/proxmoxtf/provider/provider_test.go @@ -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) diff --git a/proxmoxtf/provider/schema.go b/proxmoxtf/provider/schema.go index b66d8abb..e23ae28c 100644 --- a/proxmoxtf/provider/schema.go +++ b/proxmoxtf/provider/schema.go @@ -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, diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index e40bd1b0..e5f50f52 100644 --- a/proxmoxtf/resource/file.go +++ b/proxmoxtf/resource/file.go @@ -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 { diff --git a/tools/tools.go b/tools/tools.go index ba2ed9e1..ca989577 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -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.