github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/lfsapi/creds.go (about)

     1  package lfsapi
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/url"
     7  	"os/exec"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/git-lfs/git-lfs/errors"
    12  	"github.com/rubyist/tracerx"
    13  )
    14  
    15  // CredentialHelper is an interface used by the lfsapi Client to interact with
    16  // the 'git credential' command: https://git-scm.com/docs/gitcredentials
    17  // Other implementations include ASKPASS support, and an in-memory cache.
    18  type CredentialHelper interface {
    19  	Fill(Creds) (Creds, error)
    20  	Reject(Creds) error
    21  	Approve(Creds) error
    22  }
    23  
    24  // Creds represents a set of key/value pairs that are passed to 'git credential'
    25  // as input.
    26  type Creds map[string]string
    27  
    28  func bufferCreds(c Creds) *bytes.Buffer {
    29  	buf := new(bytes.Buffer)
    30  
    31  	for k, v := range c {
    32  		buf.Write([]byte(k))
    33  		buf.Write([]byte("="))
    34  		buf.Write([]byte(v))
    35  		buf.Write([]byte("\n"))
    36  	}
    37  
    38  	return buf
    39  }
    40  
    41  // getCredentialHelper parses a 'credsConfig' from the git and OS environments,
    42  // returning the appropriate CredentialHelper to authenticate requests with.
    43  //
    44  // It returns an error if any configuration was invalid, or otherwise
    45  // un-useable.
    46  func (c *Client) getCredentialHelper(u *url.URL) (CredentialHelper, Creds) {
    47  	rawurl := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
    48  	input := Creds{"protocol": u.Scheme, "host": u.Host}
    49  	if u.User != nil && u.User.Username() != "" {
    50  		input["username"] = u.User.Username()
    51  	}
    52  	if c.uc.Bool("credential", rawurl, "usehttppath", false) {
    53  		input["path"] = strings.TrimPrefix(u.Path, "/")
    54  	}
    55  
    56  	if c.Credentials != nil {
    57  		return c.Credentials, input
    58  	}
    59  
    60  	helpers := make([]CredentialHelper, 0, 3)
    61  	if c.cachingCredHelper != nil {
    62  		helpers = append(helpers, c.cachingCredHelper)
    63  	}
    64  	if c.askpassCredHelper != nil {
    65  		helper, _ := c.uc.Get("credential", rawurl, "helper")
    66  		if len(helper) == 0 {
    67  			helpers = append(helpers, c.askpassCredHelper)
    68  		}
    69  	}
    70  
    71  	return NewCredentialHelpers(append(helpers, c.commandCredHelper)), input
    72  }
    73  
    74  // AskPassCredentialHelper implements the CredentialHelper type for GIT_ASKPASS
    75  // and 'core.askpass' configuration values.
    76  type AskPassCredentialHelper struct {
    77  	// Program is the executable program's absolute or relative name.
    78  	Program string
    79  }
    80  
    81  // Fill implements fill by running the ASKPASS program and returning its output
    82  // as a password encoded in the Creds type given the key "password".
    83  //
    84  // It accepts the password as coming from the program's stdout, as when invoked
    85  // with the given arguments (see (*AskPassCredentialHelper).args() below)./
    86  //
    87  // If there was an error running the command, it is returned instead of a set of
    88  // filled credentials.
    89  func (a *AskPassCredentialHelper) Fill(what Creds) (Creds, error) {
    90  	var user bytes.Buffer
    91  	var pass bytes.Buffer
    92  	var err bytes.Buffer
    93  
    94  	u := &url.URL{
    95  		Scheme: what["protocol"],
    96  		Host:   what["host"],
    97  		Path:   what["path"],
    98  	}
    99  
   100  	// 'ucmd' will run the GIT_ASKPASS (or core.askpass) command prompting
   101  	// for a username.
   102  	ucmd := exec.Command(a.Program, a.args(fmt.Sprintf("Username for %q", u))...)
   103  	ucmd.Stderr = &err
   104  	ucmd.Stdout = &user
   105  
   106  	tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(ucmd.Args, " "))
   107  	if err := ucmd.Run(); err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	if err.Len() > 0 {
   112  		return nil, errors.New(err.String())
   113  	}
   114  
   115  	if username := strings.TrimSpace(user.String()); len(username) > 0 {
   116  		// If a non-empty username was given, add it to the URL via func
   117  		// 'net/url.User()'.
   118  		u.User = url.User(username)
   119  	}
   120  
   121  	// Regardless, create 'pcmd' to run the GIT_ASKPASS (or core.askpass)
   122  	// command prompting for a password.
   123  	pcmd := exec.Command(a.Program, a.args(fmt.Sprintf("Password for %q", u))...)
   124  	pcmd.Stderr = &err
   125  	pcmd.Stdout = &pass
   126  
   127  	tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(pcmd.Args, " "))
   128  	if err := pcmd.Run(); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if err.Len() > 0 {
   133  		return nil, errors.New(err.String())
   134  	}
   135  
   136  	// Finally, now that we have the username and password information,
   137  	// store it in the creds instance that we will return to the caller.
   138  	creds := make(Creds)
   139  	creds["username"] = strings.TrimSpace(user.String())
   140  	creds["password"] = strings.TrimSpace(pass.String())
   141  
   142  	return creds, nil
   143  }
   144  
   145  // Approve implements CredentialHelper.Approve, and returns nil. The ASKPASS
   146  // credential helper does not implement credential approval.
   147  func (a *AskPassCredentialHelper) Approve(_ Creds) error { return nil }
   148  
   149  // Reject implements CredentialHelper.Reject, and returns nil. The ASKPASS
   150  // credential helper does not implement credential rejection.
   151  func (a *AskPassCredentialHelper) Reject(_ Creds) error { return nil }
   152  
   153  // args returns the arguments given to the ASKPASS program, if a prompt was
   154  // given.
   155  
   156  // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
   157  // more.
   158  func (a *AskPassCredentialHelper) args(prompt string) []string {
   159  	if len(prompt) == 0 {
   160  		return nil
   161  	}
   162  	return []string{prompt}
   163  }
   164  
   165  type commandCredentialHelper struct {
   166  	SkipPrompt bool
   167  }
   168  
   169  func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
   170  	tracerx.Printf("creds: git credential fill (%q, %q, %q)",
   171  		creds["protocol"], creds["host"], creds["path"])
   172  	return h.exec("fill", creds)
   173  }
   174  
   175  func (h *commandCredentialHelper) Reject(creds Creds) error {
   176  	_, err := h.exec("reject", creds)
   177  	return err
   178  }
   179  
   180  func (h *commandCredentialHelper) Approve(creds Creds) error {
   181  	tracerx.Printf("creds: git credential approve (%q, %q, %q)",
   182  		creds["protocol"], creds["host"], creds["path"])
   183  	_, err := h.exec("approve", creds)
   184  	return err
   185  }
   186  
   187  func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, error) {
   188  	output := new(bytes.Buffer)
   189  	cmd := exec.Command("git", "credential", subcommand)
   190  	cmd.Stdin = bufferCreds(input)
   191  	cmd.Stdout = output
   192  	/*
   193  	   There is a reason we don't hook up stderr here:
   194  	   Git's credential cache daemon helper does not close its stderr, so if this
   195  	   process is the process that fires up the daemon, it will wait forever
   196  	   (until the daemon exits, really) trying to read from stderr.
   197  
   198  	   See https://github.com/git-lfs/git-lfs/issues/117 for more details.
   199  	*/
   200  
   201  	err := cmd.Start()
   202  	if err == nil {
   203  		err = cmd.Wait()
   204  	}
   205  
   206  	if _, ok := err.(*exec.ExitError); ok {
   207  		if h.SkipPrompt {
   208  			return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
   209  				input["protocol"], input["host"])
   210  		}
   211  
   212  		// 'git credential' exits with 128 if the helper doesn't fill the username
   213  		// and password values.
   214  		if subcommand == "fill" && err.Error() == "exit status 128" {
   215  			return nil, nil
   216  		}
   217  	}
   218  
   219  	if err != nil {
   220  		return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
   221  	}
   222  
   223  	creds := make(Creds)
   224  	for _, line := range strings.Split(output.String(), "\n") {
   225  		pieces := strings.SplitN(line, "=", 2)
   226  		if len(pieces) < 2 || len(pieces[1]) < 1 {
   227  			continue
   228  		}
   229  		creds[pieces[0]] = pieces[1]
   230  	}
   231  
   232  	return creds, nil
   233  }
   234  
   235  type credentialCacher struct {
   236  	creds map[string]Creds
   237  	mu    sync.Mutex
   238  }
   239  
   240  func newCredentialCacher() *credentialCacher {
   241  	return &credentialCacher{creds: make(map[string]Creds)}
   242  }
   243  
   244  func credCacheKey(creds Creds) string {
   245  	parts := []string{
   246  		creds["protocol"],
   247  		creds["host"],
   248  		creds["path"],
   249  	}
   250  	return strings.Join(parts, "//")
   251  }
   252  
   253  func (c *credentialCacher) Fill(what Creds) (Creds, error) {
   254  	key := credCacheKey(what)
   255  	c.mu.Lock()
   256  	cached, ok := c.creds[key]
   257  	c.mu.Unlock()
   258  
   259  	if ok {
   260  		tracerx.Printf("creds: git credential cache (%q, %q, %q)",
   261  			what["protocol"], what["host"], what["path"])
   262  		return cached, nil
   263  	}
   264  
   265  	return nil, credHelperNoOp
   266  }
   267  
   268  func (c *credentialCacher) Approve(what Creds) error {
   269  	key := credCacheKey(what)
   270  
   271  	c.mu.Lock()
   272  	defer c.mu.Unlock()
   273  
   274  	if _, ok := c.creds[key]; ok {
   275  		return nil
   276  	}
   277  
   278  	c.creds[key] = what
   279  	return credHelperNoOp
   280  }
   281  
   282  func (c *credentialCacher) Reject(what Creds) error {
   283  	key := credCacheKey(what)
   284  	c.mu.Lock()
   285  	delete(c.creds, key)
   286  	c.mu.Unlock()
   287  	return credHelperNoOp
   288  }
   289  
   290  // CredentialHelpers iterates through a slice of CredentialHelper objects
   291  // CredentialHelpers is a []CredentialHelper that iterates through each
   292  // credential helper to fill, reject, or approve credentials. Typically, the
   293  // first success returns immediately. Errors are reported to tracerx, unless
   294  // all credential helpers return errors. Any erroring credential helpers are
   295  // skipped for future calls.
   296  //
   297  // A CredentialHelper can return a credHelperNoOp error, signaling that the
   298  // CredentialHelpers should try the next one.
   299  type CredentialHelpers struct {
   300  	helpers        []CredentialHelper
   301  	skippedHelpers map[int]bool
   302  	mu             sync.Mutex
   303  }
   304  
   305  // NewCredentialHelpers initializes a new CredentialHelpers from the given
   306  // slice of CredentialHelper instances.
   307  func NewCredentialHelpers(helpers []CredentialHelper) CredentialHelper {
   308  	return &CredentialHelpers{
   309  		helpers:        helpers,
   310  		skippedHelpers: make(map[int]bool),
   311  	}
   312  }
   313  
   314  var credHelperNoOp = errors.New("no-op!")
   315  
   316  // Fill implements CredentialHelper.Fill by asking each CredentialHelper in
   317  // order to fill the credentials.
   318  //
   319  // If a fill was successful, it is returned immediately, and no other
   320  // `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
   321  // it is reported to tracerx, and the next one is attempted. If they all error,
   322  // then a collection of all the error messages is returned. Erroring credential
   323  // helpers are added to the skip list, and never attempted again for the
   324  // lifetime of the current Git LFS command.
   325  func (s *CredentialHelpers) Fill(what Creds) (Creds, error) {
   326  	errs := make([]string, 0, len(s.helpers))
   327  	for i, h := range s.helpers {
   328  		if s.skipped(i) {
   329  			continue
   330  		}
   331  
   332  		creds, err := h.Fill(what)
   333  		if err != nil {
   334  			if err != credHelperNoOp {
   335  				s.skip(i)
   336  				tracerx.Printf("credential fill error: %s", err)
   337  				errs = append(errs, err.Error())
   338  			}
   339  			continue
   340  		}
   341  
   342  		if creds != nil {
   343  			return creds, nil
   344  		}
   345  	}
   346  
   347  	if len(errs) > 0 {
   348  		return nil, errors.New("credential fill errors:\n" + strings.Join(errs, "\n"))
   349  	}
   350  
   351  	return nil, nil
   352  }
   353  
   354  // Reject implements CredentialHelper.Reject and rejects the given Creds "what"
   355  // with the first successful attempt.
   356  func (s *CredentialHelpers) Reject(what Creds) error {
   357  	for i, h := range s.helpers {
   358  		if s.skipped(i) {
   359  			continue
   360  		}
   361  
   362  		if err := h.Reject(what); err != credHelperNoOp {
   363  			return err
   364  		}
   365  	}
   366  
   367  	return errors.New("no valid credential helpers to reject")
   368  }
   369  
   370  // Approve implements CredentialHelper.Approve and approves the given Creds
   371  // "what" with the first successful CredentialHelper. If an error occurrs,
   372  // it calls Reject() with the same Creds and returns the error immediately. This
   373  // ensures a caching credential helper removes the cache, since the Erroring
   374  // CredentialHelper never successfully saved it.
   375  func (s *CredentialHelpers) Approve(what Creds) error {
   376  	skipped := make(map[int]bool)
   377  	for i, h := range s.helpers {
   378  		if s.skipped(i) {
   379  			skipped[i] = true
   380  			continue
   381  		}
   382  
   383  		if err := h.Approve(what); err != credHelperNoOp {
   384  			if err != nil && i > 0 { // clear any cached approvals
   385  				for j := 0; j < i; j++ {
   386  					if !skipped[j] {
   387  						s.helpers[j].Reject(what)
   388  					}
   389  				}
   390  			}
   391  			return err
   392  		}
   393  	}
   394  
   395  	return errors.New("no valid credential helpers to approve")
   396  }
   397  
   398  func (s *CredentialHelpers) skip(i int) {
   399  	s.mu.Lock()
   400  	s.skippedHelpers[i] = true
   401  	s.mu.Unlock()
   402  }
   403  
   404  func (s *CredentialHelpers) skipped(i int) bool {
   405  	s.mu.Lock()
   406  	skipped := s.skippedHelpers[i]
   407  	s.mu.Unlock()
   408  	return skipped
   409  }
   410  
   411  type nullCredentialHelper struct{}
   412  
   413  var (
   414  	nullCredError = errors.New("No credential helper configured")
   415  	nullCreds     = &nullCredentialHelper{}
   416  )
   417  
   418  func (h *nullCredentialHelper) Fill(input Creds) (Creds, error) {
   419  	return nil, nullCredError
   420  }
   421  
   422  func (h *nullCredentialHelper) Approve(creds Creds) error {
   423  	return nil
   424  }
   425  
   426  func (h *nullCredentialHelper) Reject(creds Creds) error {
   427  	return nil
   428  }