Skip to content
This repository was archived by the owner on Nov 22, 2022. It is now read-only.

Commit 925d768

Browse files
authored
Merge pull request #718 from profclems/git-credential-manager
feat: git-credential store hack
2 parents 902f80e + 65a7d41 commit 925d768

File tree

15 files changed

+588
-192
lines changed

15 files changed

+588
-192
lines changed

commands/auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func NewCmdAuth(f *cmdutils.Factory) *cobra.Command {
1515

1616
cmd.AddCommand(authLoginCmd.NewCmdLogin(f))
1717
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
18+
cmd.AddCommand(authLoginCmd.NewCmdCredential(f, nil))
1819

1920
return cmd
2021
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package authutils
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/profclems/glab/internal/run"
10+
"github.com/profclems/glab/pkg/git"
11+
"github.com/profclems/glab/pkg/prompt"
12+
13+
"github.com/AlecAivazis/survey/v2"
14+
"github.com/MakeNowJust/heredoc"
15+
"github.com/google/shlex"
16+
)
17+
18+
type GitCredentialFlow struct {
19+
Executable string
20+
21+
shouldSetup bool
22+
helper string
23+
}
24+
25+
func (gc *GitCredentialFlow) Prompt(hostname, protocol string) error {
26+
gc.helper, _ = gitCredentialHelper(hostname, protocol)
27+
if isOurCredentialHelper(gc.helper) {
28+
return nil
29+
}
30+
31+
err := prompt.AskOne(&survey.Confirm{
32+
Message: "Authenticate Git with your GitLab credentials?",
33+
Default: true,
34+
}, &gc.shouldSetup)
35+
if err != nil {
36+
return fmt.Errorf("could not prompt: %w", err)
37+
}
38+
39+
return nil
40+
}
41+
42+
func (gc *GitCredentialFlow) ShouldSetup() bool {
43+
return gc.shouldSetup
44+
}
45+
46+
func (gc *GitCredentialFlow) Setup(hostname, protocol, username, authToken string) error {
47+
return gc.gitCredentialSetup(hostname, protocol, username, authToken)
48+
}
49+
50+
func (gc *GitCredentialFlow) gitCredentialSetup(hostname, protocol, username, password string) error {
51+
if gc.helper == "" {
52+
// first use a blank value to indicate to git we want to sever the chain of credential helpers
53+
preConfigureCmd := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname, protocol), "")
54+
if err := run.PrepareCmd(preConfigureCmd).Run(); err != nil {
55+
return err
56+
}
57+
58+
// use glab as a credential helper (for this host only)
59+
configureCmd := git.GitCommand(
60+
"config", "--global", "--add",
61+
gitCredentialHelperKey(hostname, protocol),
62+
fmt.Sprintf("!%s auth git-credential", shellQuote(gc.Executable)),
63+
)
64+
return run.PrepareCmd(configureCmd).Run()
65+
}
66+
67+
// clear previous cached credentials
68+
rejectCmd := git.GitCommand("credential", "reject")
69+
70+
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
71+
protocol=%s
72+
host=%s
73+
`, protocol, hostname))
74+
75+
err := run.PrepareCmd(rejectCmd).Run()
76+
if err != nil {
77+
return err
78+
}
79+
80+
approveCmd := git.GitCommand("credential", "approve")
81+
82+
approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
83+
protocol=https
84+
host=%s
85+
username=%s
86+
password=%s
87+
`, hostname, username, password))
88+
89+
err = run.PrepareCmd(approveCmd).Run()
90+
if err != nil {
91+
return err
92+
}
93+
94+
return nil
95+
}
96+
97+
func gitCredentialHelperKey(hostname, protocol string) string {
98+
return fmt.Sprintf("credential.%s://%s.helper", protocol, hostname)
99+
}
100+
101+
func gitCredentialHelper(hostname, protocol string) (helper string, err error) {
102+
helper, err = git.Config(gitCredentialHelperKey(hostname, protocol))
103+
if helper != "" {
104+
return
105+
}
106+
helper, err = git.Config("credential.helper")
107+
return
108+
}
109+
110+
func isOurCredentialHelper(cmd string) bool {
111+
if !strings.HasPrefix(cmd, "!") {
112+
return false
113+
}
114+
115+
args, err := shlex.Split(cmd[1:])
116+
if err != nil || len(args) == 0 {
117+
return false
118+
}
119+
120+
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "glab"
121+
}
122+
123+
func shellQuote(s string) string {
124+
if strings.ContainsAny(s, " $") {
125+
return "'" + s + "'"
126+
}
127+
return s
128+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package authutils
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func Test_isOurCredentialHelper(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
arg string
11+
want bool
12+
}{
13+
{
14+
name: "looks like glab but isn't",
15+
arg: "glab auth",
16+
want: false,
17+
},
18+
{
19+
name: "ours",
20+
arg: "!/path/to/glab auth",
21+
want: true,
22+
},
23+
{
24+
name: "blank",
25+
arg: "",
26+
want: false,
27+
},
28+
{
29+
name: "invalid",
30+
arg: "!",
31+
want: false,
32+
},
33+
{
34+
name: "osxkeychain",
35+
arg: "osxkeychain",
36+
want: false,
37+
},
38+
}
39+
for _, tt := range tests {
40+
t.Run(tt.name, func(t *testing.T) {
41+
if got := isOurCredentialHelper(tt.arg); got != tt.want {
42+
t.Errorf("isOurCredentialHelper() = %v, want %v", got, tt.want)
43+
}
44+
})
45+
}
46+
}

commands/auth/login/helper.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package login
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/profclems/glab/commands/cmdutils"
10+
"github.com/profclems/glab/pkg/iostreams"
11+
12+
"github.com/spf13/cobra"
13+
)
14+
15+
const tokenUser = "oauth2"
16+
17+
type configExt interface {
18+
GetWithSource(string, string, bool) (string, string, error)
19+
}
20+
21+
type CredentialOptions struct {
22+
IO *iostreams.IOStreams
23+
Config func() (configExt, error)
24+
25+
Operation string
26+
}
27+
28+
func NewCmdCredential(f *cmdutils.Factory, runF func(*CredentialOptions) error) *cobra.Command {
29+
opts := &CredentialOptions{
30+
IO: f.IO,
31+
Config: func() (configExt, error) {
32+
return f.Config()
33+
},
34+
}
35+
36+
cmd := &cobra.Command{
37+
Use: "git-credential",
38+
Args: cobra.ExactArgs(1),
39+
Short: "Implements git credential helper manager",
40+
Hidden: true,
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
opts.Operation = args[0]
43+
44+
if runF != nil {
45+
return runF(opts)
46+
}
47+
return helperRun(opts)
48+
},
49+
}
50+
51+
return cmd
52+
}
53+
54+
func helperRun(opts *CredentialOptions) error {
55+
if opts.Operation == "store" {
56+
// We pretend to implement the "store" operation, but do nothing since we already have a cached token.
57+
return cmdutils.SilentError
58+
}
59+
60+
if opts.Operation != "get" {
61+
return fmt.Errorf("glab auth git-credential: %q is an invalid operation", opts.Operation)
62+
}
63+
64+
expectedParams := map[string]string{}
65+
66+
s := bufio.NewScanner(opts.IO.In)
67+
for s.Scan() {
68+
line := s.Text()
69+
if line == "" {
70+
break
71+
}
72+
parts := strings.SplitN(line, "=", 2)
73+
if len(parts) < 2 {
74+
continue
75+
}
76+
key, value := parts[0], parts[1]
77+
if key == "url" {
78+
u, err := url.Parse(value)
79+
if err != nil {
80+
return err
81+
}
82+
expectedParams["protocol"] = u.Scheme
83+
expectedParams["host"] = u.Host
84+
expectedParams["path"] = u.Path
85+
expectedParams["username"] = u.User.Username()
86+
expectedParams["password"], _ = u.User.Password()
87+
} else {
88+
expectedParams[key] = value
89+
}
90+
}
91+
if err := s.Err(); err != nil {
92+
return err
93+
}
94+
95+
if expectedParams["protocol"] != "https" && expectedParams["protocol"] != "http" {
96+
return cmdutils.SilentError
97+
}
98+
99+
cfg, err := opts.Config()
100+
if err != nil {
101+
return err
102+
}
103+
104+
var gotUser string
105+
gotToken, source, _ := cfg.GetWithSource(expectedParams["host"], "token", true)
106+
if strings.HasSuffix(source, "_TOKEN") {
107+
gotUser = tokenUser
108+
} else {
109+
gotUser, _, _ = cfg.GetWithSource(expectedParams["host"], "user", true)
110+
}
111+
112+
if gotUser == "" || gotToken == "" {
113+
return cmdutils.SilentError
114+
}
115+
116+
if expectedParams["username"] != "" && gotUser != tokenUser && !strings.EqualFold(expectedParams["username"], gotUser) {
117+
return cmdutils.SilentError
118+
}
119+
120+
fmt.Fprintf(opts.IO.StdOut, "protocol=%s\n", expectedParams["protocol"])
121+
fmt.Fprintf(opts.IO.StdOut, "host=%s\n", expectedParams["host"])
122+
fmt.Fprintf(opts.IO.StdOut, "username=%s\n", gotUser)
123+
fmt.Fprintf(opts.IO.StdOut, "password=%s\n", gotToken)
124+
125+
return nil
126+
}

0 commit comments

Comments
 (0)