diff --git a/fwprovider/provider_test.go b/fwprovider/provider_test.go index 73ae5c7d..e6909154 100644 --- a/fwprovider/provider_test.go +++ b/fwprovider/provider_test.go @@ -10,9 +10,11 @@ package fwprovider_test import ( "context" + "regexp" "sync" "testing" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -150,3 +152,46 @@ func TestIDGenerator_Random(t *testing.T) { require.NoError(t, err) } } + +func TestProviderAuth(t *testing.T) { + if utils.GetAnyStringEnv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled") + } + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"no credentials", []resource.TestStep{{ + Config: ` + provider "proxmox" { + api_token = "" + username = "" + } + data "proxmox_virtual_environment_version" "test" {} + `, + ExpectError: regexp.MustCompile(`must provide either username and password, an API token, or a ticket`), + }}}, + {"invalid username", []resource.TestStep{{ + Config: ` + provider "proxmox" { + api_token = "" + username = "root" + } + data "proxmox_virtual_environment_version" "test" {} + `, + ExpectError: regexp.MustCompile(`username must end with '@pve' or '@pam'`), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} diff --git a/proxmox/api/credentials.go b/proxmox/api/credentials.go index f59334a0..7f42db5e 100644 --- a/proxmox/api/credentials.go +++ b/proxmox/api/credentials.go @@ -14,6 +14,15 @@ import ( const rootUsername = "root@pam" +// Package level error declarations. +var ( + ErrMissingAPIToken = errors.New("no API token provided") + ErrMissingTicketCredentials = errors.New("no authTicket and csrfPreventionToken pair provided") + ErrMissingUserCredentials = errors.New("no username and password provided") + ErrInvalidAPIToken = errors.New("the API token must be in the format 'USER@REALM!TOKENID=UUID'") + ErrInvalidUsernameFormat = errors.New("the username must end with '@pve' or '@pam'") +) + // Credentials contains the credentials for authenticating with the Proxmox VE API. type Credentials struct { UserCredentials *UserCredentials @@ -45,31 +54,32 @@ type TicketCredentials struct { // 2. Ticket // 3. User credentials. func NewCredentials(username, password, otp, apiToken, authTicket, csrfPreventionToken string) (Credentials, error) { - tok, err := newTokenCredentials(apiToken) - if err == nil { + if tok, err := newTokenCredentials(apiToken); err == nil { return Credentials{TokenCredentials: &tok}, nil + } else if errors.Is(err, ErrInvalidAPIToken) { + return Credentials{}, err } - tic, err := newTicketCredentials(authTicket, csrfPreventionToken) - if err == nil { + if tic, err := newTicketCredentials(authTicket, csrfPreventionToken); err == nil { return Credentials{TicketCredentials: &tic}, nil } - usr, err := newUserCredentials(username, password, otp) - if err == nil { + if usr, err := newUserCredentials(username, password, otp); err == nil { return Credentials{UserCredentials: &usr}, nil + } else if errors.Is(err, ErrInvalidUsernameFormat) { + return Credentials{}, err } - return Credentials{}, errors.New("must provide either user credentials, an API token, or a ticket") + return Credentials{}, errors.New("must provide either username and password, 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") + return UserCredentials{}, ErrMissingUserCredentials } if !strings.Contains(username, "@") { - return UserCredentials{}, errors.New("username must end with '@pve' or '@pam'") + return UserCredentials{}, ErrInvalidUsernameFormat } return UserCredentials{ @@ -80,9 +90,13 @@ func newUserCredentials(username, password, otp string) (UserCredentials, error) } func newTokenCredentials(apiToken string) (TokenCredentials, error) { + if apiToken == "" { + return TokenCredentials{}, ErrMissingAPIToken + } + 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{}, ErrInvalidAPIToken } return TokenCredentials{ @@ -92,7 +106,7 @@ func newTokenCredentials(apiToken string) (TokenCredentials, error) { func newTicketCredentials(authTicket, csrfPreventionToken string) (TicketCredentials, error) { if authTicket == "" || csrfPreventionToken == "" { - return TicketCredentials{}, errors.New("both authTicket and csrfPreventionToken are required") + return TicketCredentials{}, ErrMissingTicketCredentials } return TicketCredentials{