Merge pull request #1548 from cli/auth-check

new auth flow UX
This commit is contained in:
Nate Smith 2020-08-25 10:16:23 -05:00 committed by GitHub
commit 82c6830849
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 139 additions and 18 deletions

View file

@ -10,6 +10,7 @@ import (
"path"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/command"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
@ -91,10 +92,41 @@ func main() {
}
}
authCheckEnabled := cmdutil.IsAuthCheckEnabled(cmd)
// TODO support other names
ghtoken := os.Getenv("GITHUB_TOKEN")
if ghtoken != "" {
authCheckEnabled = false
}
if authCheckEnabled {
hasAuth := false
cfg, err := cmdFactory.Config()
if err == nil {
hasAuth = cmdutil.CheckAuth(cfg)
}
if !hasAuth {
fmt.Fprintln(stderr, utils.Bold("Welcome to GitHub CLI!"))
fmt.Fprintln(stderr)
fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.")
fmt.Fprintln(stderr, "You can also set the GITHUB_TOKEN environment variable, if preferred.")
os.Exit(4)
}
}
rootCmd.SetArgs(expandedArgs)
if cmd, err := rootCmd.ExecuteC(); err != nil {
printError(stderr, err, cmd, hasDebug)
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
fmt.Println("hint: try authenticating with `gh auth login`")
}
os.Exit(1)
}
if root.HasFailed() {

View file

@ -20,6 +20,8 @@ func NewCmdAlias(f *cmdutil.Factory) *cobra.Command {
`),
}
cmdutil.DisableAuthCheck(cmd)
cmd.AddCommand(deleteCmd.NewCmdDelete(f, nil))
cmd.AddCommand(listCmd.NewCmdList(f, nil))
cmd.AddCommand(setCmd.NewCmdSet(f, nil))

View file

@ -16,6 +16,8 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
Long: `Manage gh's authentication state.`,
}
cmdutil.DisableAuthCheck(cmd)
cmd.AddCommand(authLoginCmd.NewCmdLogin(f, nil))
cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil))
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))

View file

@ -54,7 +54,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
# => read token from mytoken.txt and authenticate against github.com
$ gh auth login --hostname enterprise.internal --with-token < mytoken.txt
# => read token from mytoken.txt and authenticate against a GitHub Enterprise instance
# => read token from mytoken.txt and authenticate against a GitHub Enterprise Server instance
`),
RunE: func(cmd *cobra.Command, args []string) error {
isTTY := opts.IO.IsStdinTTY()
@ -142,7 +142,7 @@ func loginRun(opts *LoginOptions) error {
Message: "What account do you want to log into?",
Options: []string{
"GitHub.com",
"GitHub Enterprise",
"GitHub Enterprise Server",
},
}, &hostType)

View file

@ -21,6 +21,8 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
`),
}
cmdutil.DisableAuthCheck(cmd)
cmd.AddCommand(NewCmdConfigGet(f))
cmd.AddCommand(NewCmdConfigSet(f))

View file

@ -1,7 +1,6 @@
package factory
import (
"errors"
"fmt"
"net/http"
"os"
@ -30,20 +29,9 @@ func httpClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, s
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
token, err := cfg.Get(hostname, "oauth_token")
if token == "" {
var notFound *config.NotFoundError
// TODO: check if stdout is TTY too
if errors.As(err, &notFound) && io.IsStdinTTY() {
// interactive OAuth flow
token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required", nil)
}
if err != nil {
return "", err
}
if token == "" {
// TODO: instruct user how to manually authenticate
return "", fmt.Errorf("authentication required for %s", hostname)
}
if err != nil || token == "" {
// Users shouldn't see this because of the pre-execute auth check on commands
return "", fmt.Errorf("authentication required for %s; please run `gh auth login -h %s`", hostname, hostname)
}
return fmt.Sprintf("token %s", token), nil

View file

@ -72,7 +72,7 @@ func (rr *remoteResolver) Resolver() func() (context.Remotes, error) {
}
if len(cachedRemotes) == 0 {
remotesError = errors.New("none of the git remotes point to a known GitHub host")
remotesError = errors.New("none of the git remotes configured for this repository point to a known GitHub host. To tell gh about a new GitHub host, please use `gh auth login`")
return nil, remotesError
}
return cachedRemotes, nil

View file

@ -56,6 +56,8 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
},
}
cmdutil.DisableAuthCheck(cmd)
cmd.Flags().StringVarP(&shellType, "shell", "s", "", "Shell type: {bash|zsh|fish|powershell}")
return cmd

View file

@ -98,6 +98,8 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
return &cmdutil.FlagError{Err: err}
})
cmdutil.DisableAuthCheck(cmd)
// CHILD COMMANDS
cmd.AddCommand(aliasCmd.NewCmdAlias(f))

46
pkg/cmdutil/auth_check.go Normal file
View file

@ -0,0 +1,46 @@
package cmdutil
import (
"github.com/cli/cli/internal/config"
"github.com/spf13/cobra"
)
// TODO can have this set a PersistentPreRun so we don't have to set for all child commands of auth,
// config
func DisableAuthCheck(cmd *cobra.Command) {
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations["skipAuthCheck"] = "true"
}
func CheckAuth(cfg config.Config) bool {
hosts, err := cfg.Hosts()
if err != nil {
return false
}
for _, hostname := range hosts {
token, _ := cfg.Get(hostname, "oauth_token")
if token != "" {
return true
}
}
return false
}
func IsAuthCheckEnabled(cmd *cobra.Command) bool {
if !cmd.Runnable() {
return false
}
for c := cmd; c.Parent() != nil; c = c.Parent() {
if c.Annotations != nil && c.Annotations["skipAuthCheck"] == "true" {
return false
}
}
return true
}

View file

@ -0,0 +1,45 @@
package cmdutil
import (
"testing"
"github.com/cli/cli/internal/config"
"github.com/stretchr/testify/assert"
)
func Test_CheckAuth(t *testing.T) {
tests := []struct {
name string
cfg func(config.Config)
expected bool
}{
{
name: "no hosts",
cfg: func(c config.Config) {},
expected: false,
},
{
name: "host, no token",
cfg: func(c config.Config) {
_ = c.Set("github.com", "oauth_token", "")
},
expected: false,
},
{
name: "host, token",
cfg: func(c config.Config) {
_ = c.Set("github.com", "oauth_token", "a token")
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := config.NewBlankConfig()
tt.cfg(cfg)
result := CheckAuth(cfg)
assert.Equal(t, tt.expected, result)
})
}
}