github.com/git-lfs/git-lfs@v2.5.2+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  type credValueType int
    82  
    83  const (
    84  	credValueTypeUnknown credValueType = iota
    85  	credValueTypeUsername
    86  	credValueTypePassword
    87  )
    88  
    89  // Fill implements fill by running the ASKPASS program and returning its output
    90  // as a password encoded in the Creds type given the key "password".
    91  //
    92  // It accepts the password as coming from the program's stdout, as when invoked
    93  // with the given arguments (see (*AskPassCredentialHelper).args() below)./
    94  //
    95  // If there was an error running the command, it is returned instead of a set of
    96  // filled credentials.
    97  //
    98  // The ASKPASS program is only queried if a credential was not already
    99  // provided, i.e. through the git URL
   100  func (a *AskPassCredentialHelper) Fill(what Creds) (Creds, error) {
   101  	u := &url.URL{
   102  		Scheme: what["protocol"],
   103  		Host:   what["host"],
   104  		Path:   what["path"],
   105  	}
   106  
   107  	creds := make(Creds)
   108  
   109  	username, err := a.getValue(what, credValueTypeUsername, u)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	creds["username"] = username
   114  
   115  	if 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(creds["username"])
   119  	}
   120  
   121  	password, err := a.getValue(what, credValueTypePassword, u)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	creds["password"] = password
   126  
   127  	return creds, nil
   128  }
   129  
   130  func (a *AskPassCredentialHelper) getValue(what Creds, valueType credValueType, u *url.URL) (string, error) {
   131  	var valueString string
   132  
   133  	switch valueType {
   134  	case credValueTypeUsername:
   135  		valueString = "username"
   136  	case credValueTypePassword:
   137  		valueString = "password"
   138  	default:
   139  		return "", errors.Errorf("Invalid Credential type queried from AskPass")
   140  	}
   141  
   142  	// Return the existing credential if it was already provided, otherwise
   143  	// query AskPass for it
   144  	if given, ok := what[valueString]; ok {
   145  		return given, nil
   146  	}
   147  	return a.getFromProgram(valueType, u)
   148  }
   149  
   150  func (a *AskPassCredentialHelper) getFromProgram(valueType credValueType, u *url.URL) (string, error) {
   151  	var (
   152  		value bytes.Buffer
   153  		err   bytes.Buffer
   154  
   155  		valueString string
   156  	)
   157  
   158  	switch valueType {
   159  	case credValueTypeUsername:
   160  		valueString = "Username"
   161  	case credValueTypePassword:
   162  		valueString = "Password"
   163  	default:
   164  		return "", errors.Errorf("Invalid Credential type queried from AskPass")
   165  	}
   166  
   167  	// 'cmd' will run the GIT_ASKPASS (or core.askpass) command prompting
   168  	// for the desired valueType (`Username` or `Password`)
   169  	cmd := exec.Command(a.Program, a.args(fmt.Sprintf("%s for %q", valueString, u))...)
   170  	cmd.Stderr = &err
   171  	cmd.Stdout = &value
   172  
   173  	tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(cmd.Args, " "))
   174  	if err := cmd.Run(); err != nil {
   175  		return "", err
   176  	}
   177  
   178  	if err.Len() > 0 {
   179  		return "", errors.New(err.String())
   180  	}
   181  
   182  	return strings.TrimSpace(value.String()), nil
   183  }
   184  
   185  // Approve implements CredentialHelper.Approve, and returns nil. The ASKPASS
   186  // credential helper does not implement credential approval.
   187  func (a *AskPassCredentialHelper) Approve(_ Creds) error { return nil }
   188  
   189  // Reject implements CredentialHelper.Reject, and returns nil. The ASKPASS
   190  // credential helper does not implement credential rejection.
   191  func (a *AskPassCredentialHelper) Reject(_ Creds) error { return nil }
   192  
   193  // args returns the arguments given to the ASKPASS program, if a prompt was
   194  // given.
   195  
   196  // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
   197  // more.
   198  func (a *AskPassCredentialHelper) args(prompt string) []string {
   199  	if len(prompt) == 0 {
   200  		return nil
   201  	}
   202  	return []string{prompt}
   203  }
   204  
   205  type commandCredentialHelper struct {
   206  	SkipPrompt bool
   207  }
   208  
   209  func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
   210  	tracerx.Printf("creds: git credential fill (%q, %q, %q)",
   211  		creds["protocol"], creds["host"], creds["path"])
   212  	return h.exec("fill", creds)
   213  }
   214  
   215  func (h *commandCredentialHelper) Reject(creds Creds) error {
   216  	_, err := h.exec("reject", creds)
   217  	return err
   218  }
   219  
   220  func (h *commandCredentialHelper) Approve(creds Creds) error {
   221  	tracerx.Printf("creds: git credential approve (%q, %q, %q)",
   222  		creds["protocol"], creds["host"], creds["path"])
   223  	_, err := h.exec("approve", creds)
   224  	return err
   225  }
   226  
   227  func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, error) {
   228  	output := new(bytes.Buffer)
   229  	cmd := exec.Command("git", "credential", subcommand)
   230  	cmd.Stdin = bufferCreds(input)
   231  	cmd.Stdout = output
   232  	/*
   233  	   There is a reason we don't hook up stderr here:
   234  	   Git's credential cache daemon helper does not close its stderr, so if this
   235  	   process is the process that fires up the daemon, it will wait forever
   236  	   (until the daemon exits, really) trying to read from stderr.
   237  
   238  	   See https://github.com/git-lfs/git-lfs/issues/117 for more details.
   239  	*/
   240  
   241  	err := cmd.Start()
   242  	if err == nil {
   243  		err = cmd.Wait()
   244  	}
   245  
   246  	if _, ok := err.(*exec.ExitError); ok {
   247  		if h.SkipPrompt {
   248  			return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
   249  				input["protocol"], input["host"])
   250  		}
   251  
   252  		// 'git credential' exits with 128 if the helper doesn't fill the username
   253  		// and password values.
   254  		if subcommand == "fill" && err.Error() == "exit status 128" {
   255  			return nil, nil
   256  		}
   257  	}
   258  
   259  	if err != nil {
   260  		return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
   261  	}
   262  
   263  	creds := make(Creds)
   264  	for _, line := range strings.Split(output.String(), "\n") {
   265  		pieces := strings.SplitN(line, "=", 2)
   266  		if len(pieces) < 2 || len(pieces[1]) < 1 {
   267  			continue
   268  		}
   269  		creds[pieces[0]] = pieces[1]
   270  	}
   271  
   272  	return creds, nil
   273  }
   274  
   275  type credentialCacher struct {
   276  	creds map[string]Creds
   277  	mu    sync.Mutex
   278  }
   279  
   280  func newCredentialCacher() *credentialCacher {
   281  	return &credentialCacher{creds: make(map[string]Creds)}
   282  }
   283  
   284  func credCacheKey(creds Creds) string {
   285  	parts := []string{
   286  		creds["protocol"],
   287  		creds["host"],
   288  		creds["path"],
   289  	}
   290  	return strings.Join(parts, "//")
   291  }
   292  
   293  func (c *credentialCacher) Fill(what Creds) (Creds, error) {
   294  	key := credCacheKey(what)
   295  	c.mu.Lock()
   296  	cached, ok := c.creds[key]
   297  	c.mu.Unlock()
   298  
   299  	if ok {
   300  		tracerx.Printf("creds: git credential cache (%q, %q, %q)",
   301  			what["protocol"], what["host"], what["path"])
   302  		return cached, nil
   303  	}
   304  
   305  	return nil, credHelperNoOp
   306  }
   307  
   308  func (c *credentialCacher) Approve(what Creds) error {
   309  	key := credCacheKey(what)
   310  
   311  	c.mu.Lock()
   312  	defer c.mu.Unlock()
   313  
   314  	if _, ok := c.creds[key]; ok {
   315  		return nil
   316  	}
   317  
   318  	c.creds[key] = what
   319  	return credHelperNoOp
   320  }
   321  
   322  func (c *credentialCacher) Reject(what Creds) error {
   323  	key := credCacheKey(what)
   324  	c.mu.Lock()
   325  	delete(c.creds, key)
   326  	c.mu.Unlock()
   327  	return credHelperNoOp
   328  }
   329  
   330  // CredentialHelpers iterates through a slice of CredentialHelper objects
   331  // CredentialHelpers is a []CredentialHelper that iterates through each
   332  // credential helper to fill, reject, or approve credentials. Typically, the
   333  // first success returns immediately. Errors are reported to tracerx, unless
   334  // all credential helpers return errors. Any erroring credential helpers are
   335  // skipped for future calls.
   336  //
   337  // A CredentialHelper can return a credHelperNoOp error, signaling that the
   338  // CredentialHelpers should try the next one.
   339  type CredentialHelpers struct {
   340  	helpers        []CredentialHelper
   341  	skippedHelpers map[int]bool
   342  	mu             sync.Mutex
   343  }
   344  
   345  // NewCredentialHelpers initializes a new CredentialHelpers from the given
   346  // slice of CredentialHelper instances.
   347  func NewCredentialHelpers(helpers []CredentialHelper) CredentialHelper {
   348  	return &CredentialHelpers{
   349  		helpers:        helpers,
   350  		skippedHelpers: make(map[int]bool),
   351  	}
   352  }
   353  
   354  var credHelperNoOp = errors.New("no-op!")
   355  
   356  // Fill implements CredentialHelper.Fill by asking each CredentialHelper in
   357  // order to fill the credentials.
   358  //
   359  // If a fill was successful, it is returned immediately, and no other
   360  // `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
   361  // it is reported to tracerx, and the next one is attempted. If they all error,
   362  // then a collection of all the error messages is returned. Erroring credential
   363  // helpers are added to the skip list, and never attempted again for the
   364  // lifetime of the current Git LFS command.
   365  func (s *CredentialHelpers) Fill(what Creds) (Creds, error) {
   366  	errs := make([]string, 0, len(s.helpers))
   367  	for i, h := range s.helpers {
   368  		if s.skipped(i) {
   369  			continue
   370  		}
   371  
   372  		creds, err := h.Fill(what)
   373  		if err != nil {
   374  			if err != credHelperNoOp {
   375  				s.skip(i)
   376  				tracerx.Printf("credential fill error: %s", err)
   377  				errs = append(errs, err.Error())
   378  			}
   379  			continue
   380  		}
   381  
   382  		if creds != nil {
   383  			return creds, nil
   384  		}
   385  	}
   386  
   387  	if len(errs) > 0 {
   388  		return nil, errors.New("credential fill errors:\n" + strings.Join(errs, "\n"))
   389  	}
   390  
   391  	return nil, nil
   392  }
   393  
   394  // Reject implements CredentialHelper.Reject and rejects the given Creds "what"
   395  // with the first successful attempt.
   396  func (s *CredentialHelpers) Reject(what Creds) error {
   397  	for i, h := range s.helpers {
   398  		if s.skipped(i) {
   399  			continue
   400  		}
   401  
   402  		if err := h.Reject(what); err != credHelperNoOp {
   403  			return err
   404  		}
   405  	}
   406  
   407  	return errors.New("no valid credential helpers to reject")
   408  }
   409  
   410  // Approve implements CredentialHelper.Approve and approves the given Creds
   411  // "what" with the first successful CredentialHelper. If an error occurrs,
   412  // it calls Reject() with the same Creds and returns the error immediately. This
   413  // ensures a caching credential helper removes the cache, since the Erroring
   414  // CredentialHelper never successfully saved it.
   415  func (s *CredentialHelpers) Approve(what Creds) error {
   416  	skipped := make(map[int]bool)
   417  	for i, h := range s.helpers {
   418  		if s.skipped(i) {
   419  			skipped[i] = true
   420  			continue
   421  		}
   422  
   423  		if err := h.Approve(what); err != credHelperNoOp {
   424  			if err != nil && i > 0 { // clear any cached approvals
   425  				for j := 0; j < i; j++ {
   426  					if !skipped[j] {
   427  						s.helpers[j].Reject(what)
   428  					}
   429  				}
   430  			}
   431  			return err
   432  		}
   433  	}
   434  
   435  	return errors.New("no valid credential helpers to approve")
   436  }
   437  
   438  func (s *CredentialHelpers) skip(i int) {
   439  	s.mu.Lock()
   440  	s.skippedHelpers[i] = true
   441  	s.mu.Unlock()
   442  }
   443  
   444  func (s *CredentialHelpers) skipped(i int) bool {
   445  	s.mu.Lock()
   446  	skipped := s.skippedHelpers[i]
   447  	s.mu.Unlock()
   448  	return skipped
   449  }
   450  
   451  type nullCredentialHelper struct{}
   452  
   453  var (
   454  	nullCredError = errors.New("No credential helper configured")
   455  	nullCreds     = &nullCredentialHelper{}
   456  )
   457  
   458  func (h *nullCredentialHelper) Fill(input Creds) (Creds, error) {
   459  	return nil, nullCredError
   460  }
   461  
   462  func (h *nullCredentialHelper) Approve(creds Creds) error {
   463  	return nil
   464  }
   465  
   466  func (h *nullCredentialHelper) Reject(creds Creds) error {
   467  	return nil
   468  }