-
Notifications
You must be signed in to change notification settings - Fork 71
Add auth token subcommand to output active API token #1690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87e5e71
76e2c9f
35a562e
874a91a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package auth | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
|
|
||
| "github.com/fastly/cli/pkg/argparser" | ||
| fsterr "github.com/fastly/cli/pkg/errors" | ||
| "github.com/fastly/cli/pkg/global" | ||
| "github.com/fastly/cli/pkg/lookup" | ||
| "github.com/fastly/cli/pkg/text" | ||
| ) | ||
|
|
||
| // TokenCommand prints the active API token to non-terminal stdout. | ||
| type TokenCommand struct { | ||
| argparser.Base | ||
| } | ||
|
|
||
| // NewTokenCommand returns a new command registered under the parent. | ||
| func NewTokenCommand(parent argparser.Registerer, g *global.Data) *TokenCommand { | ||
| var c TokenCommand | ||
| c.Globals = g | ||
| c.CmdClause = parent.Command("token", "Output the active API token (for use in shell substitutions)") | ||
| return &c | ||
| } | ||
|
|
||
| // Exec implements the command interface. | ||
| func (c *TokenCommand) Exec(_ io.Reader, out io.Writer) error { | ||
| if text.IsTTY(out) { | ||
| return fsterr.RemediationError{ | ||
| Inner: fmt.Errorf("refusing to print token to a terminal"), | ||
| Remediation: "Use this command in a shell substitution or pipe, e.g. $(fastly auth token).", | ||
| } | ||
| } | ||
|
|
||
| token, src := c.Globals.Token() | ||
| if src == lookup.SourceUndefined || token == "" { | ||
| return fsterr.RemediationError{ | ||
| Inner: fmt.Errorf("no API token configured"), | ||
| Remediation: fsterr.ProfileRemediation(), | ||
| } | ||
| } | ||
|
|
||
| fmt.Fprint(out, token) | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package auth_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/fastly/kingpin" | ||
|
|
||
| authcmd "github.com/fastly/cli/pkg/commands/auth" | ||
| "github.com/fastly/cli/pkg/config" | ||
| fsterr "github.com/fastly/cli/pkg/errors" | ||
| "github.com/fastly/cli/pkg/global" | ||
| ) | ||
|
|
||
| func newTokenCommand(g *global.Data) *authcmd.TokenCommand { | ||
| app := kingpin.New("fastly", "test") | ||
| parent := app.Command("auth", "test auth") | ||
| return authcmd.NewTokenCommand(parent, g) | ||
| } | ||
|
|
||
| func globalDataWithToken(token string) *global.Data { | ||
| return &global.Data{ | ||
| Config: config.File{ | ||
| Auth: config.Auth{ | ||
| Default: "user", | ||
| Tokens: config.AuthTokens{ | ||
| "user": &config.AuthToken{ | ||
| Type: config.AuthTokenTypeStatic, | ||
| Token: token, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func TestToken_NonTTY_Success(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| cmd := newTokenCommand(globalDataWithToken("test-api-token-value")) | ||
| err := cmd.Exec(nil, &buf) | ||
| if err != nil { | ||
| t.Fatalf("expected no error, got: %v", err) | ||
| } | ||
| if got := buf.String(); got != "test-api-token-value" { | ||
| t.Errorf("expected token %q, got %q", "test-api-token-value", got) | ||
| } | ||
| if got := buf.Bytes(); got[len(got)-1] == '\n' { | ||
| t.Error("output should not have a trailing newline") | ||
| } | ||
| } | ||
|
|
||
| func TestToken_NonTTY_NoToken(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| g := &global.Data{ | ||
| Config: config.File{}, | ||
| } | ||
|
|
||
| cmd := newTokenCommand(g) | ||
| err := cmd.Exec(nil, &buf) | ||
| if err == nil { | ||
| t.Fatal("expected error for missing token") | ||
| } | ||
| var re fsterr.RemediationError | ||
| if !errors.As(err, &re) { | ||
| t.Fatalf("expected RemediationError, got %T: %v", err, err) | ||
| } | ||
| if re.Inner == nil || re.Inner.Error() != "no API token configured" { | ||
| t.Errorf("unexpected inner error: %v", re.Inner) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| //go:build !windows | ||
|
|
||
| package auth_test | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/creack/pty" | ||
|
|
||
| fsterr "github.com/fastly/cli/pkg/errors" | ||
| ) | ||
|
|
||
| func TestToken_TTY_Refused(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to have a separate test file for this? I don't know that we have more then 1 test file per command in the code base.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no harm in putting them in separate files, but in this case since both files are fairly small I would probably combine them. |
||
| // Create a PTY pair so we have a writable *os.File that | ||
| // term.IsTerminal recognises as a terminal. This runs reliably | ||
| // on Unix CI (no /dev/tty required) and, unlike os.Stdout, never | ||
| // risks leaking a token to the developer's real terminal. | ||
| ptm, pts, err := pty.Open() | ||
| if err != nil { | ||
| t.Fatalf("failed to open pty: %v", err) | ||
| } | ||
| defer ptm.Close() | ||
| defer pts.Close() | ||
|
|
||
| cmd := newTokenCommand(globalDataWithToken("secret-token")) | ||
| err = cmd.Exec(nil, pts) | ||
| if err == nil { | ||
| t.Fatal("expected error when stdout is a terminal") | ||
| } | ||
| var re fsterr.RemediationError | ||
| if !errors.As(err, &re) { | ||
| t.Fatalf("expected RemediationError, got %T: %v", err, err) | ||
| } | ||
| if re.Inner == nil || re.Inner.Error() != "refusing to print token to a terminal" { | ||
| t.Errorf("unexpected inner error: %v", re.Inner) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the PR # here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can also remove the last bit from the changelog entry, as it's a bit too explanatory
Refuses to print to a terminal to prevent accidental exposure..