Merge pull request #24030 from hashicorp/alisdair/terraform-login-token-validation

Add token validation for manual terraform login
This commit is contained in:
Alisdair McDiarmid 2020-02-05 10:07:28 -05:00 committed by GitHub
commit e57685d8fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 7 deletions

View File

@ -14,6 +14,7 @@ import (
"path/filepath"
"strings"
tfe "github.com/hashicorp/go-tfe"
svchost "github.com/hashicorp/terraform-svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth"
"github.com/hashicorp/terraform-svchost/disco"
@ -546,12 +547,34 @@ func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsC
}
}
token, err := c.Ui.AskSecret(fmt.Sprintf("Token for %s:", hostname.ForDisplay()))
token, err := c.Ui.AskSecret(fmt.Sprintf(c.Colorize().Color("Token for [bold]%s[reset]:"), hostname.ForDisplay()))
if err != nil {
diags := diags.Append(fmt.Errorf("Failed to retrieve token: %s", err))
return "", diags
}
token = strings.TrimSpace(token)
cfg := &tfe.Config{
Address: service.String(),
BasePath: service.Path,
Token: token,
Headers: make(http.Header),
}
client, err := tfe.NewClient(cfg)
if err != nil {
diags = diags.Append(fmt.Errorf("Failed to create API client: %s", err))
return "", diags
}
user, err := client.Users.ReadCurrent(context.Background())
if err == tfe.ErrUnauthorized {
diags = diags.Append(fmt.Errorf("Token is invalid: %s", err))
return "", diags
} else if err != nil {
diags = diags.Append(fmt.Errorf("Failed to retrieve user account details: %s", err))
return "", diags
}
c.Ui.Output(fmt.Sprintf(c.Colorize().Color("\nRetrieved token for user [bold]%s[reset]\n"), user.Username))
return svcauth.HostCredentialsToken(token), nil
}

View File

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/command/cliconfig"
oauthserver "github.com/hashicorp/terraform/command/testdata/login-oauth-server"
tfeserver "github.com/hashicorp/terraform/command/testdata/login-tfe-server"
"github.com/hashicorp/terraform/command/webbrowser"
"github.com/hashicorp/terraform/httpclient"
"github.com/hashicorp/terraform/version"
@ -27,6 +28,12 @@ func TestLogin(t *testing.T) {
s := httptest.NewServer(oauthserver.Handler)
defer s.Close()
// tfeserver.Handler is a stub TFE API implementation which will respond
// to ping and current account requests, when requests are authenticated
// with token "good-token"
ts := httptest.NewServer(tfeserver.Handler)
defer ts.Close()
loginTestCase := func(test func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string))) func(t *testing.T) {
return func(t *testing.T) {
t.Helper()
@ -73,9 +80,9 @@ func TestLogin(t *testing.T) {
svcs.ForceHostServices(svchost.Hostname("tfe.acme.com"), map[string]interface{}{
// This represents a Terraform Enterprise instance which does not
// yet support the login API, but does support the TFE tokens API.
"tfe.v2": "/api/v2",
"tfe.v2.1": "/api/v2",
"tfe.v2.2": "/api/v2",
"tfe.v2": ts.URL + "/api/v2",
"tfe.v2.1": ts.URL + "/api/v2",
"tfe.v2.2": ts.URL + "/api/v2",
})
svcs.ForceHostServices(svchost.Hostname("unsupported.example.net"), map[string]interface{}{
// This host intentionally left blank.
@ -133,8 +140,9 @@ func TestLogin(t *testing.T) {
}))
t.Run("TFE host without login support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
// Enter "yes" at the consent prompt, then paste a token.
inp("yes\npasted-token\n")
// Enter "yes" at the consent prompt, then paste a token with some
// accidental whitespace.
inp("yes\n good-token \n")
status := c.Run([]string{"tfe.acme.com"})
if status != 0 {
t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
@ -145,11 +153,29 @@ func TestLogin(t *testing.T) {
if err != nil {
t.Errorf("failed to retrieve credentials: %s", err)
}
if got, want := creds.Token(), "pasted-token"; got != want {
if got, want := creds.Token(), "good-token"; got != want {
t.Errorf("wrong token %q; want %q", got, want)
}
}))
t.Run("TFE host without login support, incorrectly pasted token", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
// Enter "yes" at the consent prompt, then paste an invalid token.
inp("yes\ngood-tok\n")
status := c.Run([]string{"tfe.acme.com"})
if status != 1 {
t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
}
credsSrc := c.Services.CredentialsSource()
creds, err := credsSrc.ForHost(svchost.Hostname("tfe.acme.com"))
if err != nil {
t.Errorf("failed to retrieve credentials: %s", err)
}
if creds != nil {
t.Errorf("wrong token %q; should have no token", creds.Token())
}
}))
t.Run("host without login or TFE API support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
status := c.Run([]string{"unsupported.example.net"})
if status == 0 {

View File

@ -0,0 +1,54 @@
// Package tfeserver is a test stub implementing a subset of the TFE API used
// only for the testing of the "terraform login" command.
package tfeserver
import (
"fmt"
"net/http"
"strings"
)
const (
goodToken = "good-token"
accountDetails = `{"data":{"id":"user-abc123","type":"users","attributes":{"username":"testuser","email":"testuser@example.com"}}}`
)
// Handler is an implementation of net/http.Handler that provides a stub
// TFE API server implementation with the following endpoints:
//
// /ping - API existence endpoint
// /account/details - current user endpoint
var Handler http.Handler
type handler struct{}
func (h handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Type", "application/vnd.api+json")
switch req.URL.Path {
case "/api/v2/ping":
h.servePing(resp, req)
case "/api/v2/account/details":
h.serveAccountDetails(resp, req)
default:
fmt.Printf("404 when fetching %s\n", req.URL.String())
http.Error(resp, `{"errors":[{"status":"404","title":"not found"}]}`, http.StatusNotFound)
}
}
func (h handler) servePing(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusNoContent)
}
func (h handler) serveAccountDetails(resp http.ResponseWriter, req *http.Request) {
if !strings.Contains(req.Header.Get("Authorization"), goodToken) {
http.Error(resp, `{"errors":[{"status":"401","title":"unauthorized"}]}`, http.StatusUnauthorized)
return
}
resp.WriteHeader(http.StatusOK)
resp.Write([]byte(accountDetails))
}
func init() {
Handler = handler{}
}