2016-12-19 21:38:06 +00:00
|
|
|
package lfsapi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
2017-08-09 16:26:21 +00:00
|
|
|
"sync"
|
2017-03-24 17:27:52 +00:00
|
|
|
|
2017-08-10 22:24:46 +00:00
|
|
|
"github.com/git-lfs/git-lfs/config"
|
2017-08-10 22:49:46 +00:00
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
2017-08-10 22:30:42 +00:00
|
|
|
"github.com/git-lfs/git-lfs/tools"
|
2017-03-24 17:27:52 +00:00
|
|
|
"github.com/rubyist/tracerx"
|
2016-12-19 21:38:06 +00:00
|
|
|
)
|
|
|
|
|
2017-08-10 22:24:46 +00:00
|
|
|
// credsConfig supplies configuration options pertaining to the authorization
|
|
|
|
// process in package lfsapi.
|
|
|
|
type credsConfig struct {
|
2017-08-10 22:30:42 +00:00
|
|
|
// AskPass is a string containing an executable name as well as a
|
|
|
|
// program arguments.
|
|
|
|
//
|
|
|
|
// See: https://git-scm.com/docs/gitcredentials#_requesting_credentials
|
|
|
|
// for more.
|
|
|
|
AskPass string `os:"GIT_ASKPASS" git:"core.askpass"`
|
2017-08-10 22:24:46 +00:00
|
|
|
// Cached is a boolean determining whether or not to enable the
|
|
|
|
// credential cacher.
|
|
|
|
Cached bool `git:"lfs.cachecredentials"`
|
|
|
|
// SkipPrompt is a boolean determining whether or not to prompt the user
|
|
|
|
// for a password.
|
|
|
|
SkipPrompt bool `os:"GIT_TERMINAL_PROMPT"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// getCredentialHelper parses a 'credsConfig' from the git and OS environments,
|
|
|
|
// returning the appropriate CredentialHelper to authenticate requests with.
|
|
|
|
//
|
|
|
|
// It returns an error if any configuration was invalid, or otherwise
|
|
|
|
// un-useable.
|
|
|
|
func getCredentialHelper(cfg *config.Configuration) (CredentialHelper, error) {
|
|
|
|
ccfg, err := getCredentialConfig(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-08-10 22:30:42 +00:00
|
|
|
var hs []CredentialHelper
|
|
|
|
if len(ccfg.AskPass) > 0 {
|
|
|
|
parts := tools.QuotedFields(ccfg.AskPass)
|
|
|
|
if len(parts) < 1 {
|
|
|
|
return nil, errors.Errorf(
|
|
|
|
"lfsapi/creds: invalid ASKPASS: %q",
|
|
|
|
ccfg.AskPass)
|
|
|
|
}
|
|
|
|
|
|
|
|
hs = append(hs, &AskPassCredentialHelper{
|
|
|
|
Program: parts[0],
|
|
|
|
Args: parts[1:],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-08-10 22:24:46 +00:00
|
|
|
var h CredentialHelper
|
|
|
|
h = &commandCredentialHelper{
|
|
|
|
SkipPrompt: ccfg.SkipPrompt,
|
|
|
|
}
|
|
|
|
|
|
|
|
if ccfg.Cached {
|
|
|
|
h = withCredentialCache(h)
|
|
|
|
}
|
2017-08-10 22:30:42 +00:00
|
|
|
hs = append(hs, h)
|
2017-08-10 22:24:46 +00:00
|
|
|
|
2017-08-10 22:30:42 +00:00
|
|
|
switch len(hs) {
|
|
|
|
case 0:
|
|
|
|
return nil, nil
|
|
|
|
case 1:
|
|
|
|
return hs[0], nil
|
|
|
|
}
|
|
|
|
return CredentialHelpers(hs), nil
|
2017-08-10 22:24:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// getCredentialConfig parses a *credsConfig given the OS and Git
|
|
|
|
// configurations.
|
|
|
|
func getCredentialConfig(cfg *config.Configuration) (*credsConfig, error) {
|
|
|
|
var what credsConfig
|
|
|
|
|
|
|
|
if err := cfg.Unmarshal(&what); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &what, nil
|
|
|
|
}
|
|
|
|
|
2017-08-10 22:49:31 +00:00
|
|
|
// CredentialHelpers is a []CredentialHelper that iterates through each
|
|
|
|
// credential helper to fill, reject, or approve credentials.
|
|
|
|
type CredentialHelpers []CredentialHelper
|
|
|
|
|
|
|
|
// Fill implements CredentialHelper.Fill by asking each CredentialHelper in
|
|
|
|
// order to fill the credentials.
|
|
|
|
//
|
|
|
|
// If a fill was successful, it is returned immediately, and no other
|
|
|
|
// `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
|
|
|
|
// it is returned immediately.
|
|
|
|
func (h CredentialHelpers) Fill(what Creds) (Creds, error) {
|
|
|
|
for _, c := range h {
|
|
|
|
creds, err := c.Fill(what)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if creds != nil {
|
|
|
|
return creds, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reject implements CredentialHelper.Reject and rejects the given Creds "what"
|
|
|
|
// amongst all knonw CredentialHelpers. If any `CredentialHelper`s returned a
|
|
|
|
// non-nil error, no further `CredentialHelper`s are notified, so as to prevent
|
|
|
|
// inconsistent state.
|
|
|
|
func (h CredentialHelpers) Reject(what Creds) error {
|
|
|
|
for _, c := range h {
|
|
|
|
if err := c.Reject(what); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Approve implements CredentialHelper.Approve and approves the given Creds
|
|
|
|
// "what" amongst all knonw CredentialHelpers. If any `CredentialHelper`s
|
|
|
|
// returned a non-nil error, no further `CredentialHelper`s are notified, so as
|
|
|
|
// to prevent inconsistent state.
|
|
|
|
func (h CredentialHelpers) Approve(what Creds) error {
|
|
|
|
for _, c := range h {
|
|
|
|
if err := c.Approve(what); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-08-10 22:49:46 +00:00
|
|
|
// AskPassCredentialHelper implements the CredentialHelper type for GIT_ASKPASS
|
|
|
|
// and 'core.askpass' configuration values.
|
|
|
|
type AskPassCredentialHelper struct {
|
|
|
|
// Program is the executable program's absolute or relative name.
|
|
|
|
Program string
|
|
|
|
// Args are the arguments given to the program.
|
|
|
|
Args []string
|
|
|
|
|
|
|
|
// Prompt is an optional prompt appended to the end of the program's
|
|
|
|
// arguments, if given. This is implemented for consistency with the Git
|
|
|
|
// documentation.
|
|
|
|
Prompt string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fill implements fill by running the ASKPASS program and returning its output
|
|
|
|
// as a password encoded in the Creds type given the key "password".
|
|
|
|
//
|
|
|
|
// It accepts the password as coming from the program's stdout, as when invoked
|
|
|
|
// with the given arguments (see (*AskPassCredentialHelper).args() below)./
|
|
|
|
//
|
|
|
|
// If there was an error running the command, it is returned instead of a set of
|
|
|
|
// filled credentials.
|
|
|
|
func (a *AskPassCredentialHelper) Fill(what Creds) (Creds, error) {
|
|
|
|
var pass bytes.Buffer
|
|
|
|
var err bytes.Buffer
|
|
|
|
|
|
|
|
cmd := exec.Command(a.Program, a.args()...)
|
|
|
|
cmd.Stderr = &err
|
|
|
|
cmd.Stdout = &pass
|
|
|
|
|
|
|
|
tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(cmd.Args, " "))
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err.Len() > 0 {
|
|
|
|
return nil, errors.New(err.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
creds := make(Creds)
|
|
|
|
creds["password"] = pass.String()
|
|
|
|
|
|
|
|
return creds, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Approve implements CredentialHelper.Approve, and returns nil. The ASKPASS
|
|
|
|
// credential helper does not implement credential approval.
|
|
|
|
func (a *AskPassCredentialHelper) Approve(_ Creds) error { return nil }
|
|
|
|
|
|
|
|
// Reject implements CredentialHelper.Reject, and returns nil. The ASKPASS
|
|
|
|
// credential helper does not implement credential rejection.
|
|
|
|
func (a *AskPassCredentialHelper) Reject(_ Creds) error { return nil }
|
|
|
|
|
|
|
|
// args returns the arguments given to the ASKPASS program. If a prompt (see:
|
|
|
|
// "Prompt string") is given, it is appended as the final argument. Otherwise,
|
|
|
|
// the arguments are passed to the program as is.
|
|
|
|
|
|
|
|
// See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
|
|
|
|
// more.
|
|
|
|
func (a *AskPassCredentialHelper) args() []string {
|
|
|
|
if len(a.Prompt) == 0 {
|
|
|
|
return a.Args
|
|
|
|
}
|
|
|
|
return append(a.Args, a.Prompt)
|
|
|
|
}
|
|
|
|
|
2016-12-19 21:38:06 +00:00
|
|
|
type CredentialHelper interface {
|
|
|
|
Fill(Creds) (Creds, error)
|
|
|
|
Reject(Creds) error
|
|
|
|
Approve(Creds) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type Creds map[string]string
|
|
|
|
|
2017-01-06 21:42:37 +00:00
|
|
|
func bufferCreds(c Creds) *bytes.Buffer {
|
2016-12-19 21:38:06 +00:00
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
for k, v := range c {
|
|
|
|
buf.Write([]byte(k))
|
|
|
|
buf.Write([]byte("="))
|
|
|
|
buf.Write([]byte(v))
|
|
|
|
buf.Write([]byte("\n"))
|
|
|
|
}
|
|
|
|
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
2017-03-24 17:27:52 +00:00
|
|
|
func withCredentialCache(helper CredentialHelper) CredentialHelper {
|
|
|
|
return &credentialCacher{
|
2017-08-09 16:26:21 +00:00
|
|
|
cmu: new(sync.Mutex),
|
2017-03-24 17:27:52 +00:00
|
|
|
creds: make(map[string]Creds),
|
|
|
|
helper: helper,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type credentialCacher struct {
|
2017-08-09 16:26:21 +00:00
|
|
|
// cmu guards creds
|
|
|
|
cmu *sync.Mutex
|
2017-03-24 17:27:52 +00:00
|
|
|
creds map[string]Creds
|
|
|
|
helper CredentialHelper
|
|
|
|
}
|
|
|
|
|
|
|
|
func credCacheKey(creds Creds) string {
|
|
|
|
parts := []string{
|
|
|
|
creds["protocol"],
|
|
|
|
creds["host"],
|
|
|
|
creds["path"],
|
|
|
|
}
|
|
|
|
return strings.Join(parts, "//")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *credentialCacher) Fill(creds Creds) (Creds, error) {
|
|
|
|
key := credCacheKey(creds)
|
2017-08-09 16:26:21 +00:00
|
|
|
|
|
|
|
c.cmu.Lock()
|
|
|
|
defer c.cmu.Unlock()
|
|
|
|
|
2017-03-24 17:27:52 +00:00
|
|
|
if cache, ok := c.creds[key]; ok {
|
|
|
|
tracerx.Printf("creds: git credential cache (%q, %q, %q)",
|
|
|
|
creds["protocol"], creds["host"], creds["path"])
|
|
|
|
return cache, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
creds, err := c.helper.Fill(creds)
|
|
|
|
if err == nil && len(creds["username"]) > 0 && len(creds["password"]) > 0 {
|
|
|
|
c.creds[key] = creds
|
|
|
|
}
|
|
|
|
return creds, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *credentialCacher) Reject(creds Creds) error {
|
2017-08-09 16:26:21 +00:00
|
|
|
c.cmu.Lock()
|
|
|
|
defer c.cmu.Unlock()
|
|
|
|
|
2017-03-24 17:27:52 +00:00
|
|
|
delete(c.creds, credCacheKey(creds))
|
|
|
|
return c.helper.Reject(creds)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *credentialCacher) Approve(creds Creds) error {
|
|
|
|
err := c.helper.Approve(creds)
|
|
|
|
if err == nil {
|
2017-08-09 16:26:21 +00:00
|
|
|
c.cmu.Lock()
|
2017-03-24 17:27:52 +00:00
|
|
|
c.creds[credCacheKey(creds)] = creds
|
2017-08-09 16:26:21 +00:00
|
|
|
c.cmu.Unlock()
|
2017-03-24 17:27:52 +00:00
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-01-06 21:38:57 +00:00
|
|
|
type commandCredentialHelper struct {
|
2016-12-19 21:38:06 +00:00
|
|
|
SkipPrompt bool
|
|
|
|
}
|
|
|
|
|
2017-01-06 21:38:57 +00:00
|
|
|
func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
|
2017-03-24 17:27:52 +00:00
|
|
|
tracerx.Printf("creds: git credential fill (%q, %q, %q)",
|
|
|
|
creds["protocol"], creds["host"], creds["path"])
|
2016-12-19 21:38:06 +00:00
|
|
|
return h.exec("fill", creds)
|
|
|
|
}
|
|
|
|
|
2017-01-06 21:38:57 +00:00
|
|
|
func (h *commandCredentialHelper) Reject(creds Creds) error {
|
2016-12-19 21:38:06 +00:00
|
|
|
_, err := h.exec("reject", creds)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-01-06 21:38:57 +00:00
|
|
|
func (h *commandCredentialHelper) Approve(creds Creds) error {
|
2016-12-19 21:38:06 +00:00
|
|
|
_, err := h.exec("approve", creds)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-01-06 21:38:57 +00:00
|
|
|
func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, error) {
|
2016-12-19 21:38:06 +00:00
|
|
|
output := new(bytes.Buffer)
|
|
|
|
cmd := exec.Command("git", "credential", subcommand)
|
2017-01-06 21:42:37 +00:00
|
|
|
cmd.Stdin = bufferCreds(input)
|
2016-12-19 21:38:06 +00:00
|
|
|
cmd.Stdout = output
|
|
|
|
/*
|
|
|
|
There is a reason we don't hook up stderr here:
|
|
|
|
Git's credential cache daemon helper does not close its stderr, so if this
|
|
|
|
process is the process that fires up the daemon, it will wait forever
|
|
|
|
(until the daemon exits, really) trying to read from stderr.
|
|
|
|
|
|
|
|
See https://github.com/git-lfs/git-lfs/issues/117 for more details.
|
|
|
|
*/
|
|
|
|
|
|
|
|
err := cmd.Start()
|
|
|
|
if err == nil {
|
|
|
|
err = cmd.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := err.(*exec.ExitError); ok {
|
|
|
|
if h.SkipPrompt {
|
|
|
|
return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
|
|
|
|
input["protocol"], input["host"])
|
|
|
|
}
|
|
|
|
|
|
|
|
// 'git credential' exits with 128 if the helper doesn't fill the username
|
|
|
|
// and password values.
|
|
|
|
if subcommand == "fill" && err.Error() == "exit status 128" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
creds := make(Creds)
|
|
|
|
for _, line := range strings.Split(output.String(), "\n") {
|
|
|
|
pieces := strings.SplitN(line, "=", 2)
|
|
|
|
if len(pieces) < 2 || len(pieces[1]) < 1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
creds[pieces[0]] = pieces[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return creds, nil
|
|
|
|
}
|