This repository has been archived by the owner on Nov 22, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #718 from profclems/git-credential-manager
feat: git-credential store hack
- Loading branch information
Showing
15 changed files
with
588 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package authutils | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/profclems/glab/internal/run" | ||
"github.com/profclems/glab/pkg/git" | ||
"github.com/profclems/glab/pkg/prompt" | ||
|
||
"github.com/AlecAivazis/survey/v2" | ||
"github.com/MakeNowJust/heredoc" | ||
"github.com/google/shlex" | ||
) | ||
|
||
type GitCredentialFlow struct { | ||
Executable string | ||
|
||
shouldSetup bool | ||
helper string | ||
} | ||
|
||
func (gc *GitCredentialFlow) Prompt(hostname, protocol string) error { | ||
gc.helper, _ = gitCredentialHelper(hostname, protocol) | ||
if isOurCredentialHelper(gc.helper) { | ||
return nil | ||
} | ||
|
||
err := prompt.AskOne(&survey.Confirm{ | ||
Message: "Authenticate Git with your GitLab credentials?", | ||
Default: true, | ||
}, &gc.shouldSetup) | ||
if err != nil { | ||
return fmt.Errorf("could not prompt: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (gc *GitCredentialFlow) ShouldSetup() bool { | ||
return gc.shouldSetup | ||
} | ||
|
||
func (gc *GitCredentialFlow) Setup(hostname, protocol, username, authToken string) error { | ||
return gc.gitCredentialSetup(hostname, protocol, username, authToken) | ||
} | ||
|
||
func (gc *GitCredentialFlow) gitCredentialSetup(hostname, protocol, username, password string) error { | ||
if gc.helper == "" { | ||
// first use a blank value to indicate to git we want to sever the chain of credential helpers | ||
preConfigureCmd := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname, protocol), "") | ||
if err := run.PrepareCmd(preConfigureCmd).Run(); err != nil { | ||
return err | ||
} | ||
|
||
// use glab as a credential helper (for this host only) | ||
configureCmd := git.GitCommand( | ||
"config", "--global", "--add", | ||
gitCredentialHelperKey(hostname, protocol), | ||
fmt.Sprintf("!%s auth git-credential", shellQuote(gc.Executable)), | ||
) | ||
return run.PrepareCmd(configureCmd).Run() | ||
} | ||
|
||
// clear previous cached credentials | ||
rejectCmd := git.GitCommand("credential", "reject") | ||
|
||
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` | ||
protocol=%s | ||
host=%s | ||
`, protocol, hostname)) | ||
|
||
err := run.PrepareCmd(rejectCmd).Run() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
approveCmd := git.GitCommand("credential", "approve") | ||
|
||
approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` | ||
protocol=https | ||
host=%s | ||
username=%s | ||
password=%s | ||
`, hostname, username, password)) | ||
|
||
err = run.PrepareCmd(approveCmd).Run() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func gitCredentialHelperKey(hostname, protocol string) string { | ||
return fmt.Sprintf("credential.%s://%s.helper", protocol, hostname) | ||
} | ||
|
||
func gitCredentialHelper(hostname, protocol string) (helper string, err error) { | ||
helper, err = git.Config(gitCredentialHelperKey(hostname, protocol)) | ||
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") == "glab" | ||
} | ||
|
||
func shellQuote(s string) string { | ||
if strings.ContainsAny(s, " $") { | ||
return "'" + s + "'" | ||
} | ||
return s | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package authutils | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func Test_isOurCredentialHelper(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
arg string | ||
want bool | ||
}{ | ||
{ | ||
name: "looks like glab but isn't", | ||
arg: "glab auth", | ||
want: false, | ||
}, | ||
{ | ||
name: "ours", | ||
arg: "!/path/to/glab auth", | ||
want: true, | ||
}, | ||
{ | ||
name: "blank", | ||
arg: "", | ||
want: false, | ||
}, | ||
{ | ||
name: "invalid", | ||
arg: "!", | ||
want: false, | ||
}, | ||
{ | ||
name: "osxkeychain", | ||
arg: "osxkeychain", | ||
want: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := isOurCredentialHelper(tt.arg); got != tt.want { | ||
t.Errorf("isOurCredentialHelper() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package login | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/profclems/glab/commands/cmdutils" | ||
"github.com/profclems/glab/pkg/iostreams" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
const tokenUser = "oauth2" | ||
|
||
type configExt interface { | ||
GetWithSource(string, string, bool) (string, string, error) | ||
} | ||
|
||
type CredentialOptions struct { | ||
IO *iostreams.IOStreams | ||
Config func() (configExt, error) | ||
|
||
Operation string | ||
} | ||
|
||
func NewCmdCredential(f *cmdutils.Factory, runF func(*CredentialOptions) error) *cobra.Command { | ||
opts := &CredentialOptions{ | ||
IO: f.IO, | ||
Config: func() (configExt, error) { | ||
return f.Config() | ||
}, | ||
} | ||
|
||
cmd := &cobra.Command{ | ||
Use: "git-credential", | ||
Args: cobra.ExactArgs(1), | ||
Short: "Implements git credential helper manager", | ||
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 cmdutils.SilentError | ||
} | ||
|
||
if opts.Operation != "get" { | ||
return fmt.Errorf("glab auth git-credential: %q is an invalid operation", opts.Operation) | ||
} | ||
|
||
expectedParams := 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 | ||
} | ||
key, value := parts[0], parts[1] | ||
if key == "url" { | ||
u, err := url.Parse(value) | ||
if err != nil { | ||
return err | ||
} | ||
expectedParams["protocol"] = u.Scheme | ||
expectedParams["host"] = u.Host | ||
expectedParams["path"] = u.Path | ||
expectedParams["username"] = u.User.Username() | ||
expectedParams["password"], _ = u.User.Password() | ||
} else { | ||
expectedParams[key] = value | ||
} | ||
} | ||
if err := s.Err(); err != nil { | ||
return err | ||
} | ||
|
||
if expectedParams["protocol"] != "https" && expectedParams["protocol"] != "http" { | ||
return cmdutils.SilentError | ||
} | ||
|
||
cfg, err := opts.Config() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var gotUser string | ||
gotToken, source, _ := cfg.GetWithSource(expectedParams["host"], "token", true) | ||
if strings.HasSuffix(source, "_TOKEN") { | ||
gotUser = tokenUser | ||
} else { | ||
gotUser, _, _ = cfg.GetWithSource(expectedParams["host"], "user", true) | ||
} | ||
|
||
if gotUser == "" || gotToken == "" { | ||
return cmdutils.SilentError | ||
} | ||
|
||
if expectedParams["username"] != "" && gotUser != tokenUser && !strings.EqualFold(expectedParams["username"], gotUser) { | ||
return cmdutils.SilentError | ||
} | ||
|
||
fmt.Fprintf(opts.IO.StdOut, "protocol=%s\n", expectedParams["protocol"]) | ||
fmt.Fprintf(opts.IO.StdOut, "host=%s\n", expectedParams["host"]) | ||
fmt.Fprintf(opts.IO.StdOut, "username=%s\n", gotUser) | ||
fmt.Fprintf(opts.IO.StdOut, "password=%s\n", gotToken) | ||
|
||
return nil | ||
} |
Oops, something went wrong.