cli/pkg/cmd/auth/login/login.go
Mislav Marohnić 98f1f5ec0d Use absolute path when configuring gh as git credential
This keeps git operations working even when PATH is modified, e.g. `brew
update` will work even though Homebrew runs the command explicitly
without `/usr/local/bin` in PATH.

Additionally, this inserts a blank value for `credential.*.helper` to
instruct git to ignore previously configured credential helpers, i.e.
those that might have been set up in system configuration files. We do
this because otherwise, git will store the credential obtained from gh
in every other credential helper in the chain, which we want to avoid.

Before:

    git config --global credential.https://github.com.helper '!gh auth git-credential'

After:

    git config --global credential.https://github.com.helper ''
    git config --global --add credential.https://github.com.helper '!/path/to/gh auth git-credential'
2021-03-03 16:20:21 +01:00

227 lines
5.8 KiB
Go

package login
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/cmd/auth/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/spf13/cobra"
)
type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
MainExecutable string
Interactive bool
Hostname string
Scopes []string
Token string
Web bool
}
func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
opts := &LoginOptions{
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
MainExecutable: f.Executable,
}
var tokenStdin bool
cmd := &cobra.Command{
Use: "login",
Args: cobra.ExactArgs(0),
Short: "Authenticate with a GitHub host",
Long: heredoc.Docf(`
Authenticate with a GitHub host.
The default authentication mode is a web-based browser flow.
Alternatively, pass in a token on standard input by using %[1]s--with-token%[1]s.
The minimum required scopes for the token are: "repo", "read:org".
The --scopes flag accepts a comma separated list of scopes you want your gh credentials to have. If
absent, this command ensures that gh has access to a minimum set of scopes.
`, "`"),
Example: heredoc.Doc(`
# start interactive setup
$ gh auth login
# authenticate against github.com by reading the token from a file
$ gh auth login --with-token < mytoken.txt
# authenticate with a specific GitHub Enterprise Server instance
$ gh auth login --hostname enterprise.internal
`),
RunE: func(cmd *cobra.Command, args []string) error {
if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) {
return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")}
}
if tokenStdin && opts.Web {
return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")}
}
if tokenStdin {
defer opts.IO.In.Close()
token, err := ioutil.ReadAll(opts.IO.In)
if err != nil {
return fmt.Errorf("failed to read token from STDIN: %w", err)
}
opts.Token = strings.TrimSpace(string(token))
}
if opts.IO.CanPrompt() && opts.Token == "" && !opts.Web {
opts.Interactive = true
}
if cmd.Flags().Changed("hostname") {
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
}
}
if !opts.Interactive {
if opts.Hostname == "" {
opts.Hostname = ghinstance.Default()
}
}
if runF != nil {
return runF(opts)
}
return loginRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with")
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input")
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate")
return cmd
}
func loginRun(opts *LoginOptions) error {
cfg, err := opts.Config()
if err != nil {
return err
}
hostname := opts.Hostname
if hostname == "" {
if opts.Interactive {
var err error
hostname, err = promptForHostname()
if err != nil {
return err
}
} else {
return errors.New("must specify --hostname")
}
}
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
var roErr *config.ReadOnlyEnvError
if errors.As(err, &roErr) {
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n")
return cmdutil.SilentError
}
return err
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
if opts.Token != "" {
err := cfg.Set(hostname, "oauth_token", opts.Token)
if err != nil {
return err
}
if err := shared.HasMinimumScopes(httpClient, hostname, opts.Token); err != nil {
return fmt.Errorf("error validating token: %w", err)
}
return cfg.Write()
}
existingToken, _ := cfg.Get(hostname, "oauth_token")
if existingToken != "" && opts.Interactive {
if err := shared.HasMinimumScopes(httpClient, hostname, existingToken); err == nil {
var keepGoing bool
err = prompt.SurveyAskOne(&survey.Confirm{
Message: fmt.Sprintf(
"You're already logged into %s. Do you want to re-authenticate?",
hostname),
Default: false,
}, &keepGoing)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
if !keepGoing {
return nil
}
}
}
return shared.Login(&shared.LoginOptions{
IO: opts.IO,
Config: cfg,
HTTPClient: httpClient,
Hostname: hostname,
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
Executable: opts.MainExecutable,
})
}
func promptForHostname() (string, error) {
var hostType int
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to log into?",
Options: []string{
"GitHub.com",
"GitHub Enterprise Server",
},
}, &hostType)
if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
isEnterprise := hostType == 1
hostname := ghinstance.Default()
if isEnterprise {
err := prompt.SurveyAskOne(&survey.Input{
Message: "GHE hostname:",
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
}
return hostname, nil
}