From d56d92c908cb2454d9b592916e980754fa310041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 23 Nov 2020 20:19:18 +0100 Subject: [PATCH] If git credential helper is non-defined, set gh as credential helper --- internal/config/config_type.go | 3 +- pkg/cmd/auth/auth.go | 2 + pkg/cmd/auth/gitcredential/helper.go | 112 +++++++++++++++++++++++++++ pkg/cmd/auth/login/login.go | 102 ++++++++++++++++++------ 4 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 pkg/cmd/auth/gitcredential/helper.go diff --git a/internal/config/config_type.go b/internal/config/config_type.go index 40533f211..4c04d9f6b 100644 --- a/internal/config/config_type.go +++ b/internal/config/config_type.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sort" + "strings" "github.com/cli/cli/internal/ghinstance" "gopkg.in/yaml.v3" @@ -378,7 +379,7 @@ func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) { } for _, hc := range hosts { - if hc.Host == hostname { + if strings.EqualFold(hc.Host, hostname) { return hc, nil } } diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index ddeb07aa6..6909f99f8 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + gitCredentialCmd "github.com/cli/cli/pkg/cmd/auth/gitcredential" authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login" authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout" authRefreshCmd "github.com/cli/cli/pkg/cmd/auth/refresh" @@ -22,6 +23,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil)) cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil)) cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil)) + cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil)) return cmd } diff --git a/pkg/cmd/auth/gitcredential/helper.go b/pkg/cmd/auth/gitcredential/helper.go new file mode 100644 index 000000000..a9dbcbad9 --- /dev/null +++ b/pkg/cmd/auth/gitcredential/helper.go @@ -0,0 +1,112 @@ +package login + +import ( + "bufio" + "fmt" + "net/url" + "strings" + + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/spf13/cobra" +) + +type CredentialOptions struct { + IO *iostreams.IOStreams + Config func() (config.Config, error) + + Operation string +} + +func NewCmdCredential(f *cmdutil.Factory, runF func(*CredentialOptions) error) *cobra.Command { + opts := &CredentialOptions{ + IO: f.IOStreams, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "git-credential", + Args: cobra.ExactArgs(1), + Short: "Implements git credential helper protocol", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + opts.Operation = args[0] + + if runF != nil { + return runF(opts) + } + + return helperRun(opts) + }, + } + + return cmd +} + +func helperRun(opts *CredentialOptions) error { + if opts.Operation == "store" { + // We pretend to implement the "store" operation, but do nothing since we already have a cached token. + return cmdutil.SilentError + } + + if opts.Operation != "get" { + return fmt.Errorf("gh auth git-credential: %q operation not supported", opts.Operation) + } + + wants := map[string]string{} + + s := bufio.NewScanner(opts.IO.In) + for s.Scan() { + line := s.Text() + if line == "" { + break + } + parts := strings.SplitN(line, "=", 2) + if len(parts) < 2 { + continue + } + wants[parts[0]] = parts[1] + } + if err := s.Err(); err != nil { + return err + } + + if uv := wants["url"]; uv != "" { + u, err := url.Parse(uv) + if err != nil { + return err + } + wants["protocol"] = u.Scheme + wants["host"] = u.Host + wants["path"] = u.Path + wants["username"] = u.User.Username() + wants["password"], _ = u.User.Password() + } + + if wants["protocol"] != "https" { + return cmdutil.SilentError + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + gotUser, _ := cfg.Get(wants["host"], "user") + gotToken, _ := cfg.Get(wants["host"], "oauth_token") + if gotUser == "" || gotToken == "" { + return cmdutil.SilentError + } + + if wants["username"] != "" && !strings.EqualFold(wants["username"], gotUser) { + return cmdutil.SilentError + } + + fmt.Fprint(opts.IO.Out, "protocol=https\n") + fmt.Fprintf(opts.IO.Out, "host=%s\n", wants["host"]) + fmt.Fprintf(opts.IO.Out, "username=%s\n", gotUser) + fmt.Fprintf(opts.IO.Out, "password=%s\n", gotToken) + + return nil +} diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 4246d5d8b..f92e6e7f3 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "path/filepath" "strings" "github.com/AlecAivazis/survey/v2" @@ -19,6 +20,7 @@ import ( "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" + "github.com/google/shlex" "github.com/spf13/cobra" ) @@ -314,32 +316,62 @@ func loginRun(opts *LoginOptions) error { } if opts.Interactive && gitProtocol == "https" { - var primeCredentials bool - err = prompt.SurveyAskOne(&survey.Confirm{ - Message: "Set up git for passwordless push/pull operations?", - Default: true, - }, &primeCredentials) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } - if primeCredentials { - gitCredential, err := git.GitCommand("credential", "approve") + helper, _ := gitCredentialHelper(hostname) + if !isOurCredentialHelper(helper) { + var primeCredentials bool + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Set up git for passwordless push/pull operations?", + Default: true, + }, &primeCredentials) if err != nil { - return err + return fmt.Errorf("could not prompt: %w", err) } - credentialStdin := &bytes.Buffer{} - gitCredential.Stdin = credentialStdin - password, _ := cfg.Get(hostname, "oauth_token") - fmt.Fprint(credentialStdin, "protocol=https\n") - fmt.Fprintf(credentialStdin, "host=%s\n", hostname) - fmt.Fprintf(credentialStdin, "username=%s\n", username) - fmt.Fprintf(credentialStdin, "password=%s\n", password) - fmt.Fprint(credentialStdin, "\n") + if primeCredentials { + if helper == "" { + configureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "!gh auth git-credential") + if err != nil { + return err + } - err = run.PrepareCmd(gitCredential).Run() - if err != nil { - return err + err = run.PrepareCmd(configureCmd).Run() + if err != nil { + return err + } + } else { + rejectCmd, err := git.GitCommand("credential", "reject") + if err != nil { + return err + } + + rejectCmd.Stdin = bytes.NewBufferString(fmt.Sprintf(heredoc.Doc(` + protocol=https + host=%s + `), hostname)) + + err = run.PrepareCmd(rejectCmd).Run() + if err != nil { + return err + } + + approveCmd, err := git.GitCommand("credential", "approve") + if err != nil { + return err + } + + password, _ := cfg.Get(hostname, "oauth_token") + approveCmd.Stdin = bytes.NewBufferString(fmt.Sprintf(heredoc.Doc(` + protocol=https + host=%s + username=%s + password=%s + `), hostname, username, password)) + + err = run.PrepareCmd(approveCmd).Run() + if err != nil { + return err + } + } } } } @@ -358,3 +390,29 @@ func getAccessTokenTip(hostname string) string { Tip: you can generate a Personal Access Token here https://%s/settings/tokens The minimum required scopes are 'repo' and 'read:org'.`, ghHostname) } + +func gitCredentialHelperKey(hostname string) string { + return fmt.Sprintf("credential.https://%s.helper", hostname) +} + +func gitCredentialHelper(hostname string) (helper string, err error) { + helper, err = git.Config(gitCredentialHelperKey(hostname)) + if helper != "" { + return + } + helper, err = git.Config("credential.helper") + return +} + +func isOurCredentialHelper(cmd string) bool { + if !strings.HasPrefix(cmd, "!") { + return false + } + + args, err := shlex.Split(cmd[1:]) + if err != nil || len(args) == 0 { + return false + } + + return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh" +}