Skip to content

Commit

Permalink
Modernize auth init prompt. (#1337)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewsomething authored Jan 6, 2023
1 parent 87d470d commit 31aa0f4
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 107 deletions.
53 changes: 34 additions & 19 deletions commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ import (
"path/filepath"
"sort"
"strings"
"syscall"

"github.com/digitalocean/doctl"

"golang.org/x/crypto/ssh/terminal"
"github.com/digitalocean/doctl/commands/charm/input"
"github.com/digitalocean/doctl/commands/charm/template"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
yaml "gopkg.in/yaml.v2"
)

const (
// TokenValidationServer is the default server used to validate an OAuth token
TokenValidationServer = "https://cloud.digitalocean.com"

legacyTokenLength = 64
v1TokenLength = 71
)

// ErrUnknownTerminal signifies an unknown terminal. It is returned when doit
Expand All @@ -47,18 +50,30 @@ var (
// retrieveUserTokenFromCommandLine is a function that can retrieve a token. By default,
// it will prompt the user. In test, you can replace this with code that returns the appropriate response.
func retrieveUserTokenFromCommandLine() (string, error) {
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return "", ErrUnknownTerminal
}

fmt.Print("Please authenticate doctl for use with your DigitalOcean account. You can generate a token in the control panel at https://cloud.digitalocean.com/account/api/tokens\n\n")
fmt.Print("Enter your access token: ")
passwdBytes, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", err
}
template.Print(
`Please authenticate doctl for use with your DigitalOcean account. You can generate a token in the control panel at {{underline "https://cloud.digitalocean.com/account/api/tokens"}}{{nl}}{{nl}}`,
nil,
)

prompt := input.New("Enter your access token: ",
input.WithHidden(),
input.WithRequired(),
input.WithValidator(tokenInputValidator),
)

return string(passwdBytes), nil
return prompt.Prompt()
}

// tokenInputValidator is used to do basic validation of a token while it is being typed.
func tokenInputValidator(input string) error {
if len(input) == legacyTokenLength || (strings.HasPrefix(input, "do") && len(input) == v1TokenLength) {
return nil
}
return errors.New("")
}

// UnknownSchemeError signifies an unknown HTTP scheme.
Expand Down Expand Up @@ -131,6 +146,10 @@ To create new contexts, see the help for `+"`"+`doctl auth init`+"`"+`.`, Writer
func RunAuthInit(retrieveUserTokenFunc func() (string, error)) func(c *CmdConfig) error {
return func(c *CmdConfig) error {
token := c.getContextAccessToken()
context := Context
if context == "" {
context = viper.GetString("context")
}

if token == "" {
in, err := retrieveUserTokenFunc()
Expand All @@ -139,14 +158,12 @@ func RunAuthInit(retrieveUserTokenFunc func() (string, error)) func(c *CmdConfig
}
token = strings.TrimSpace(in)
} else {
fmt.Fprintf(c.Out, "Using token [%v]", token)
fmt.Fprintln(c.Out)
template.Render(c.Out, `Using token for context {{highlight .}}{{nl}}`, context)
}

c.setContextAccessToken(token)

fmt.Fprintln(c.Out)
fmt.Fprint(c.Out, "Validating token... ")
template.Render(c.Out, `{{nl}}Validating token... `, nil)

// need to initial the godo client since we've changed the configuration.
if err := c.initServices(c); err != nil {
Expand All @@ -159,13 +176,11 @@ func RunAuthInit(retrieveUserTokenFunc func() (string, error)) func(c *CmdConfig
}

if _, err := c.OAuth().TokenInfo(server); err != nil {
fmt.Fprintln(c.Out, "invalid token")
fmt.Fprintln(c.Out)
template.Render(c.Out, `{{error crossmark}}{{nl}}{{nl}}`, nil)
return fmt.Errorf("Unable to use supplied token to access API: %s", err)
}

fmt.Fprintln(c.Out, "OK")
fmt.Fprintln(c.Out)
template.Render(c.Out, `{{success checkmark}}{{nl}}{{nl}}`, nil)

return writeConfig()
}
Expand Down
57 changes: 57 additions & 0 deletions commands/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,63 @@ func Test_displayAuthContexts(t *testing.T) {
}
}

func TestTokenInputValidator(t *testing.T) {
tests := []struct {
name string
token string
valid bool
}{
{
name: "valid legacy token",
token: "53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84",
valid: true,
},
{
name: "valid v1 pat",
token: "dop_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84",
valid: true,
},
{
name: "valid v1 oauth",
token: "doo_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84",
valid: true,
},
{
name: "too short legacy token",
token: "53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adca",
},
{
name: "too long legacy token",
token: "53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84a2d45",
},
{
name: "too short v1 pat",
token: "dop_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae",
},
{
name: "too short v1 oauth",
token: "doo_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adc84",
},
{
name: "too long v1 pat",
token: "dop_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84sdsd",
},
{
name: "too long v1 oauth",
token: "doo_v1_53918d3cd735062ca6ea791427900af10cf595f18dc6016c1cb0c3a11adcae84sd",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.valid {
assert.NoError(t, tokenInputValidator(tt.token))
} else {
assert.Error(t, tokenInputValidator(tt.name))
}
})
}
}

type testConfig map[string]interface{}

type nopWriteCloser struct {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.12.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
golang.org/x/term v0.3.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -116,7 +117,6 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.1.12 // indirect
Expand Down
46 changes: 36 additions & 10 deletions integration/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/creack/pty"
"github.com/sclevine/spec"
"github.com/stretchr/testify/require"
"golang.org/x/term"
)

var _ = suite("auth/init", func(t *testing.T, when spec.G, it spec.S) {
Expand All @@ -37,7 +38,8 @@ var _ = suite("auth/init", func(t *testing.T, when spec.G, it spec.S) {
case "/v1/oauth/token/info":
auth := req.Header.Get("Authorization")

if auth == "Bearer first-token" || auth == "Bearer second-token" || auth == "Bearer some-magic-token" {
if auth == "Bearer first-token" || auth == "Bearer second-token" ||
auth == "Bearer some-magic-token" || auth == "Bearer some-magic-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1" {
w.Write([]byte(`{"resource_owner_id":123}`))
return
}
Expand Down Expand Up @@ -79,22 +81,30 @@ var _ = suite("auth/init", func(t *testing.T, when spec.G, it spec.S) {
ptmx, err := pty.Start(cmd)
expect.NoError(err)

// Set the terminal to raw mode so that we can send the carriage return
fd := int(ptmx.Fd())
oldState, err := term.MakeRaw(fd)
if err != nil {
panic(err)
}
defer func() { _ = term.Restore(fd, oldState) }()

go func() {
ptmx.Write([]byte("some-magic-token\n"))
ptmx.Write([]byte("some-magic-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1\r"))
}()

buf := bytes.NewBuffer([]byte{})

count, _ := io.Copy(buf, ptmx) // yes, ignore error intentionally
expect.NotZero(count)
ptmx.Close()

expect.Contains(buf.String(), "Validating token... OK")
expect.Contains(buf.String(), "Validating token...")
expect.Contains(buf.String(), "✔")

fileBytes, err := ioutil.ReadFile(testConfig)
expect.NoError(err)

expect.Contains(string(fileBytes), "access-token: some-magic-token")
expect.Contains(string(fileBytes), "access-token: some-magic-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1")
})
})

Expand Down Expand Up @@ -161,9 +171,16 @@ context: default

ptmx, err := pty.Start(cmd)
expect.NoError(err)
// Set the terminal to raw mode so that we can send the carriage return
fd := int(ptmx.Fd())
oldState, err := term.MakeRaw(fd)
if err != nil {
panic(err)
}
defer func() { _ = term.Restore(fd, oldState) }()

go func() {
ptmx.Write([]byte("some-magic-token\n"))
ptmx.Write([]byte("some-magic-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1\r"))
}()

buf := bytes.NewBuffer([]byte{})
Expand All @@ -172,15 +189,16 @@ context: default
expect.NotZero(count)
ptmx.Close()

expect.Contains(buf.String(), "Validating token... OK")
expect.Contains(buf.String(), "Validating token...")
expect.Contains(buf.String(), "✔")

location, err := getDefaultConfigLocation()
expect.NoError(err)

fileBytes, err := ioutil.ReadFile(location)
expect.NoError(err)

expect.Contains(string(fileBytes), "access-token: some-magic-token")
expect.Contains(string(fileBytes), "access-token: some-magic-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1")

err = os.Remove(location)
expect.NoError(err)
Expand All @@ -203,9 +221,16 @@ context: default

ptmx, err := pty.Start(cmd)
expect.NoError(err)
// Set the terminal to raw mode so that we can send the carriage return
fd := int(ptmx.Fd())
oldState, err := term.MakeRaw(fd)
if err != nil {
panic(err)
}
defer func() { _ = term.Restore(fd, oldState) }()

go func() {
ptmx.Write([]byte("some-bad-token\n"))
ptmx.Write([]byte("some-bad-token-that-is-64-characters-long-1a1a1a1a1a11a1a1a1a1a1\r"))
}()

buf := bytes.NewBuffer([]byte{})
Expand All @@ -214,7 +239,8 @@ context: default
expect.NotZero(count)
ptmx.Close()

expect.Contains(buf.String(), "Validating token... invalid token")
expect.Contains(buf.String(), "Validating token...")
expect.Contains(buf.String(), "✘")
expect.Contains(buf.String(), fmt.Sprintf("Unable to use supplied token to access API: GET %s/v1/oauth/token/info: 401", server.URL))
})
})
Expand Down
76 changes: 0 additions & 76 deletions vendor/golang.org/x/crypto/ssh/terminal/terminal.go

This file was deleted.

1 change: 0 additions & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ golang.org/x/crypto/internal/poly1305
golang.org/x/crypto/internal/subtle
golang.org/x/crypto/ssh
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/ssh/terminal
# golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
## explicit; go 1.17
golang.org/x/mod/internal/lazyregexp
Expand Down

0 comments on commit 31aa0f4

Please sign in to comment.