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

Commit

Permalink
Merge pull request #718 from profclems/git-credential-manager
Browse files Browse the repository at this point in the history
feat: git-credential store hack
  • Loading branch information
profclems authored May 14, 2021
2 parents 902f80e + 65a7d41 commit 925d768
Show file tree
Hide file tree
Showing 15 changed files with 588 additions and 192 deletions.
1 change: 1 addition & 0 deletions commands/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func NewCmdAuth(f *cmdutils.Factory) *cobra.Command {

cmd.AddCommand(authLoginCmd.NewCmdLogin(f))
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
cmd.AddCommand(authLoginCmd.NewCmdCredential(f, nil))

return cmd
}
128 changes: 128 additions & 0 deletions commands/auth/authutils/git_credentials.go
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
}
46 changes: 46 additions & 0 deletions commands/auth/authutils/git_credentials_test.go
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)
}
})
}
}
126 changes: 126 additions & 0 deletions commands/auth/login/helper.go
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
}
Loading

0 comments on commit 925d768

Please sign in to comment.