Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

### Enhancements:

- feat(auth): add `auth token` subcommand to output the active API token for use in shell substitutions (e.g. `$(fastly auth token)`). Refuses to print to a terminal to prevent accidental exposure.
Comment thread
jedisct1 marked this conversation as resolved.
Outdated
- feat(auth): `auth login --sso` now requires `--token <name>` to explicitly name the stored token. This prevents accidentally overwriting tokens in multi-user SSO workflows. [#1676](https://github.com/fastly/cli/pull/1676)
- feat(auth): add `FASTLY_DISABLE_AUTH_COMMAND` env var to hide the `fastly auth` command tree from help, completions, and invocation. [#1676](https://github.com/fastly/cli/pull/1676)
- feat(auth): when `FASTLY_DISABLE_AUTH_COMMAND` is set, the `--token`/`-t` global flag is also disabled. Use `FASTLY_API_TOKEN` or stored config tokens instead. [#1676](https://github.com/fastly/cli/pull/1676)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (

require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dnaeon/go-vcr v1.2.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down
46 changes: 46 additions & 0 deletions pkg/commands/auth/token.go
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
}
71 changes: 71 additions & 0 deletions pkg/commands/auth/token_test.go
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)
}
}
38 changes: 38 additions & 0 deletions pkg/commands/auth/token_tty_unix_test.go
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) {
Comment thread
rcaril marked this conversation as resolved.
// 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)
}
}
3 changes: 2 additions & 1 deletion pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,10 @@ func Define( // nolint:revive // function-length
authList := authcmd.NewListCommand(authCmdRoot.CmdClause, data)
authShow := authcmd.NewShowCommand(authCmdRoot.CmdClause, data)
authUse := authcmd.NewUseCommand(authCmdRoot.CmdClause, data)
authToken := authcmd.NewTokenCommand(authCmdRoot.CmdClause, data)
authCommands = []argparser.Command{
authCmdRoot, authLogin, authAdd, authDelete,
authList, authShow, authUse,
authList, authShow, authUse, authToken,
}

authtokenCmdRoot := authtoken.NewRootCommand(app, data)
Expand Down
Loading