cli/pkg/cmd/auth/shared/login_flow.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

208 lines
5.1 KiB
Go

package shared
import (
"fmt"
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/authflow"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
)
type iconfig interface {
Get(string, string) (string, error)
Set(string, string, string) error
Write() error
}
type LoginOptions struct {
IO *iostreams.IOStreams
Config iconfig
HTTPClient *http.Client
Hostname string
Interactive bool
Web bool
Scopes []string
Executable string
sshContext sshContext
}
func Login(opts *LoginOptions) error {
cfg := opts.Config
hostname := opts.Hostname
httpClient := opts.HTTPClient
cs := opts.IO.ColorScheme()
var gitProtocol string
if opts.Interactive {
var proto string
err := prompt.SurveyAskOne(&survey.Select{
Message: "What is your preferred protocol for Git operations?",
Options: []string{
"HTTPS",
"SSH",
},
}, &proto)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
gitProtocol = strings.ToLower(proto)
}
var additionalScopes []string
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
return err
}
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
}
var keyToUpload string
if opts.Interactive && gitProtocol == "ssh" {
pubKeys, err := opts.sshContext.localPublicKeys()
if err != nil {
return err
}
if len(pubKeys) > 0 {
var keyChoice int
err := prompt.SurveyAskOne(&survey.Select{
Message: "Upload your SSH public key to your GitHub account?",
Options: append(pubKeys, "Skip"),
}, &keyChoice)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
if keyChoice < len(pubKeys) {
keyToUpload = pubKeys[keyChoice]
}
} else {
var err error
keyToUpload, err = opts.sshContext.generateSSHKey()
if err != nil {
return err
}
}
}
if keyToUpload != "" {
additionalScopes = append(additionalScopes, "admin:public_key")
}
var authMode int
if opts.Web {
authMode = 0
} else {
err := prompt.SurveyAskOne(&survey.Select{
Message: "How would you like to authenticate GitHub CLI?",
Options: []string{
"Login with a web browser",
"Paste an authentication token",
},
}, &authMode)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
}
var authToken string
userValidated := false
if authMode == 0 {
var err error
authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...))
if err != nil {
return fmt.Errorf("failed to authenticate via web browser: %w", err)
}
userValidated = true
} else {
minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...)
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
Tip: you can generate a Personal Access Token here https://%s/settings/tokens
The minimum required scopes are %s.
`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
err := prompt.SurveyAskOne(&survey.Password{
Message: "Paste your authentication token:",
}, &authToken, survey.WithValidator(survey.Required))
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil {
return fmt.Errorf("error validating token: %w", err)
}
if err := cfg.Set(hostname, "oauth_token", authToken); err != nil {
return err
}
}
var username string
if userValidated {
username, _ = cfg.Get(hostname, "user")
} else {
apiClient := api.NewClientFromHTTP(httpClient)
var err error
username, err = api.CurrentLoginName(apiClient, hostname)
if err != nil {
return fmt.Errorf("error using api: %w", err)
}
err = cfg.Set(hostname, "user", username)
if err != nil {
return err
}
}
if gitProtocol != "" {
fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol)
err := cfg.Set(hostname, "git_protocol", gitProtocol)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon())
}
err := cfg.Write()
if err != nil {
return err
}
if credentialFlow.ShouldSetup() {
err := credentialFlow.Setup(hostname, username, authToken)
if err != nil {
return err
}
}
if keyToUpload != "" {
err := sshKeyUpload(httpClient, hostname, keyToUpload)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload))
}
fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username))
return nil
}
func scopesSentence(scopes []string, isEnterprise bool) string {
quoted := make([]string, len(scopes))
for i, s := range scopes {
quoted[i] = fmt.Sprintf("'%s'", s)
if s == "workflow" && isEnterprise {
// remove when GHE 2.x reaches EOL
quoted[i] += " (GHE 3.0+)"
}
}
return strings.Join(quoted, ", ")
}