Fixes non-interactive login flow and make sure "prompt" configuration is respected by never prompting if it was explicitly disabled. No longer asks to press Enter again after "Authentication complete" message, since that didn't provide any value to the user.
209 lines
5.2 KiB
Go
209 lines
5.2 KiB
Go
package shared
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/internal/authflow"
|
|
"github.com/cli/cli/v2/internal/ghinstance"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/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 if opts.Interactive {
|
|
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...), opts.Interactive)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to authenticate via web browser: %w", err)
|
|
}
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon())
|
|
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, ", ")
|
|
}
|