gh auth refresh

This commit is contained in:
vilmibm 2020-08-07 12:37:58 -05:00
parent 55a13a32b4
commit f1c0d04bc0
3 changed files with 377 additions and 0 deletions

View file

@ -62,6 +62,7 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co
}
func logoutRun(opts *LogoutOptions) error {
// TODO check for GITHUB_TOKEN and error if found
isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
hostname := opts.Hostname

View file

@ -0,0 +1,127 @@
package refresh
import (
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/spf13/cobra"
)
type RefreshOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Hostname string
Scopes []string
}
func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command {
opts := &RefreshOptions{
IO: f.IOStreams,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "refresh",
Args: cobra.ExactArgs(0),
Short: "Request new scopes for a token",
Long: heredoc.Doc(`Expand the permission scopes for a given host's token.
This command allows you to add additional scopes to an existing authentication token via a web
browser. This enables gh to access more of the GitHub API, which may be required as gh adds
features or as you use the gh api command.
Unfortunately at this time there is no way to add scopes without a web browser's involvement
due to how GitHub authentication works.
The --hostname flag allows you to operate on a GitHub host other than github.com.
The --scopes flag accepts a comma separated list of scopes you want to add to a token. If
absent, this command ensures that a host's token has the default set of scopes required by gh.
Note that if GITHUB_TOKEN is in the current environment, this command will not work.
`),
Example: heredoc.Doc(`
$ gh auth refresh --scopes write:org,read:public_key
# => open a browser to add write:org and read:public_key scopes for use with gh api
$ gh auth refresh
# => ensure that the required minimum scopes are enabled for a token and open a browser to add if not
`),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)
}
return refreshRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication")
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", []string{}, "Additional scopes to add to a token")
return cmd
}
func refreshRun(opts *RefreshOptions) error {
if os.Getenv("GITHUB_TOKEN") != "" {
return fmt.Errorf("GITHUB_TOKEN is present in your environment and is incompatible with this command. If you'd like to modify a personal access token, see https://github.com/settings/tokens")
}
isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
if !isTTY {
return fmt.Errorf("not attached to a terminal; in headless environments, GITHUB_TOKEN is recommended")
}
cfg, err := opts.Config()
if err != nil {
return err
}
candidates, err := cfg.Hosts()
if err != nil {
return fmt.Errorf("not logged in to any hosts. Use 'gh auth login' to authenticate with a host")
}
hostname := opts.Hostname
if hostname == "" {
if len(candidates) == 1 {
hostname = candidates[0]
} else {
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to refresh auth for?",
Options: candidates,
}, &hostname)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
}
} else {
var found bool
for _, c := range candidates {
if c == hostname {
found = true
break
}
}
if !found {
return fmt.Errorf("not logged in to %s. use 'gh auth login' to authenticate with this host", hostname)
}
}
return doAuthFlow(cfg, hostname, opts.Scopes)
}
var doAuthFlow = func(cfg config.Config, hostname string, scopes []string) error {
_, err := config.AuthFlowWithConfig(cfg, hostname, "", scopes)
return err
}

View file

@ -0,0 +1,249 @@
package refresh
import (
"bytes"
"os"
"regexp"
"testing"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func Test_NewCmdRefresh(t *testing.T) {
tests := []struct {
name string
cli string
wants RefreshOptions
}{
{
name: "no arguments",
wants: RefreshOptions{
Hostname: "",
Scopes: []string{},
},
},
{
name: "hostname",
cli: "-h aline.cedrac",
wants: RefreshOptions{
Hostname: "aline.cedrac",
Scopes: []string{},
},
},
{
name: "one scope",
cli: "--scopes repo:invite",
wants: RefreshOptions{
Scopes: []string{"repo:invite"},
},
},
{
name: "scopes",
cli: "--scopes repo:invite,read:public_key",
wants: RefreshOptions{
Scopes: []string{"repo:invite", "read:public_key"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
var gotOpts *RefreshOptions
cmd := NewCmdRefresh(f, func(opts *RefreshOptions) error {
gotOpts = opts
return nil
})
// TODO cobra hack-around
cmd.Flags().BoolP("help", "x", false, "")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
assert.NoError(t, err)
assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname)
assert.Equal(t, tt.wants.Scopes, gotOpts.Scopes)
})
}
}
type authArgs struct {
hostname string
scopes []string
}
func Test_refreshRun(t *testing.T) {
tests := []struct {
name string
opts *RefreshOptions
askStubs func(*prompt.AskStubber)
cfgHosts []string
wantErr *regexp.Regexp
ghtoken string
nontty bool
wantAuthArgs authArgs
}{
{
name: "GITHUB_TOKEN set",
opts: &RefreshOptions{},
ghtoken: "abc123",
wantErr: regexp.MustCompile(`GITHUB_TOKEN is present in your environment`),
},
{
name: "non tty",
opts: &RefreshOptions{},
nontty: true,
wantErr: regexp.MustCompile(`not attached to a terminal;`),
},
{
name: "no hosts configured",
opts: &RefreshOptions{},
wantErr: regexp.MustCompile(`not logged in to any hosts`),
},
{
name: "hostname given but dne",
cfgHosts: []string{
"github.com",
"aline.cedrac",
},
opts: &RefreshOptions{
Hostname: "obed.morton",
},
wantErr: regexp.MustCompile(`not logged in to obed.morton`),
},
{
name: "hostname provided and is configured",
cfgHosts: []string{
"obed.morton",
"github.com",
},
opts: &RefreshOptions{
Hostname: "obed.morton",
Scopes: []string{},
},
wantAuthArgs: authArgs{
hostname: "obed.morton",
scopes: []string{},
},
},
{
name: "no hostname, one host configured",
cfgHosts: []string{
"github.com",
},
opts: &RefreshOptions{
Hostname: "",
Scopes: []string{},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{},
},
},
{
name: "no hostname, multiple hosts configured",
cfgHosts: []string{
"github.com",
"aline.cedrac",
},
opts: &RefreshOptions{
Hostname: "",
Scopes: []string{},
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne("github.com")
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{},
},
},
{
name: "scopes provided",
cfgHosts: []string{
"github.com",
},
opts: &RefreshOptions{
Scopes: []string{"repo:invite", "public_key:read"},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{"repo:invite", "public_key:read"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
aa := authArgs{}
doAuthFlow = func(_ config.Config, hostname string, scopes []string) error {
aa.hostname = hostname
aa.scopes = scopes
return nil
}
ghtoken := os.Getenv("GITHUB_TOKEN")
defer func() {
os.Setenv("GITHUB_TOKEN", ghtoken)
}()
os.Setenv("GITHUB_TOKEN", tt.ghtoken)
io, _, _, _ := iostreams.Test()
io.SetStdinTTY(!tt.nontty)
io.SetStdoutTTY(!tt.nontty)
tt.opts.IO = io
cfg := config.NewBlankConfig()
tt.opts.Config = func() (config.Config, error) {
return cfg, nil
}
for _, hostname := range tt.cfgHosts {
_ = cfg.Set(hostname, "oauth_token", "abc123")
}
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"login":"cybilb"}}}`))
mainBuf := bytes.Buffer{}
hostsBuf := bytes.Buffer{}
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
as, teardown := prompt.InitAskStubber()
defer teardown()
if tt.askStubs != nil {
tt.askStubs(as)
}
err := refreshRun(tt.opts)
assert.Equal(t, tt.wantErr == nil, err == nil)
if err != nil {
if tt.wantErr != nil {
assert.True(t, tt.wantErr.MatchString(err.Error()))
return
} else {
t.Fatalf("unexpected error: %s", err)
}
}
assert.Equal(t, aa.hostname, tt.wantAuthArgs.hostname)
assert.Equal(t, aa.scopes, tt.wantAuthArgs.scopes)
})
}
}