11 "github.com/git-lfs/git-lfs/errors"
12 "github.com/rubyist/tracerx"
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)
24 // Creds represents a set of key/value pairs that are passed to 'git credential'
26 type Creds map[string]string
28 func bufferCreds(c Creds) *bytes.Buffer {
29 buf := new(bytes.Buffer)
33 buf.Write([]byte("="))
35 buf.Write([]byte("\n"))
41 // getCredentialHelper parses a 'credsConfig' from the git and OS environments,
42 // returning the appropriate CredentialHelper to authenticate requests with.
44 // It returns an error if any configuration was invalid, or otherwise
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()
52 if c.uc.Bool("credential", rawurl, "usehttppath", false) {
53 input["path"] = strings.TrimPrefix(u.Path, "/")
56 if c.Credentials != nil {
57 return c.Credentials, input
60 helpers := make([]CredentialHelper, 0, 3)
61 if c.cachingCredHelper != nil {
62 helpers = append(helpers, c.cachingCredHelper)
64 if c.askpassCredHelper != nil {
65 helper, _ := c.uc.Get("credential", rawurl, "helper")
67 helpers = append(helpers, c.askpassCredHelper)
71 return NewCredentialHelpers(append(helpers, c.commandCredHelper)), input
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.
81 type credValueType int
84 credValueTypeUnknown credValueType = iota
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".
92 // It accepts the password as coming from the program's stdout, as when invoked
93 // with the given arguments (see (*AskPassCredentialHelper).args() below)./
95 // If there was an error running the command, it is returned instead of a set of
96 // filled credentials.
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) {
102 Scheme: what["protocol"],
109 username, err := a.getValue(what, credValueTypeUsername, u)
113 creds["username"] = username
115 if len(username) > 0 {
116 // If a non-empty username was given, add it to the URL via func
118 u.User = url.User(creds["username"])
121 password, err := a.getValue(what, credValueTypePassword, u)
125 creds["password"] = password
130 func (a *AskPassCredentialHelper) getValue(what Creds, valueType credValueType, u *url.URL) (string, error) {
131 var valueString string
134 case credValueTypeUsername:
135 valueString = "username"
136 case credValueTypePassword:
137 valueString = "password"
139 return "", errors.Errorf("Invalid Credential type queried from AskPass")
142 // Return the existing credential if it was already provided, otherwise
143 // query AskPass for it
144 if given, ok := what[valueString]; ok {
147 return a.getFromProgram(valueType, u)
150 func (a *AskPassCredentialHelper) getFromProgram(valueType credValueType, u *url.URL) (string, error) {
159 case credValueTypeUsername:
160 valueString = "Username"
161 case credValueTypePassword:
162 valueString = "Password"
164 return "", errors.Errorf("Invalid Credential type queried from AskPass")
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))...)
173 tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(cmd.Args, " "))
174 if err := cmd.Run(); err != nil {
179 return "", errors.New(err.String())
182 return strings.TrimSpace(value.String()), nil
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 }
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 }
193 // args returns the arguments given to the ASKPASS program, if a prompt was
196 // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
198 func (a *AskPassCredentialHelper) args(prompt string) []string {
199 if len(prompt) == 0 {
202 return []string{prompt}
205 type commandCredentialHelper struct {
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)
215 func (h *commandCredentialHelper) Reject(creds Creds) error {
216 _, err := h.exec("reject", creds)
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)
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)
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.
238 See https://github.com/git-lfs/git-lfs/issues/117 for more details.
246 if _, ok := err.(*exec.ExitError); ok {
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"])
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" {
260 return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
264 for _, line := range strings.Split(output.String(), "\n") {
265 pieces := strings.SplitN(line, "=", 2)
266 if len(pieces) < 2 || len(pieces[1]) < 1 {
269 creds[pieces[0]] = pieces[1]
275 type credentialCacher struct {
276 creds map[string]Creds
280 func newCredentialCacher() *credentialCacher {
281 return &credentialCacher{creds: make(map[string]Creds)}
284 func credCacheKey(creds Creds) string {
290 return strings.Join(parts, "//")
293 func (c *credentialCacher) Fill(what Creds) (Creds, error) {
294 key := credCacheKey(what)
296 cached, ok := c.creds[key]
300 tracerx.Printf("creds: git credential cache (%q, %q, %q)",
301 what["protocol"], what["host"], what["path"])
305 return nil, credHelperNoOp
308 func (c *credentialCacher) Approve(what Creds) error {
309 key := credCacheKey(what)
314 if _, ok := c.creds[key]; ok {
319 return credHelperNoOp
322 func (c *credentialCacher) Reject(what Creds) error {
323 key := credCacheKey(what)
327 return credHelperNoOp
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.
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
345 // NewCredentialHelpers initializes a new CredentialHelpers from the given
346 // slice of CredentialHelper instances.
347 func NewCredentialHelpers(helpers []CredentialHelper) CredentialHelper {
348 return &CredentialHelpers{
350 skippedHelpers: make(map[int]bool),
354 var credHelperNoOp = errors.New("no-op!")
356 // Fill implements CredentialHelper.Fill by asking each CredentialHelper in
357 // order to fill the credentials.
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 {
372 creds, err := h.Fill(what)
374 if err != credHelperNoOp {
376 tracerx.Printf("credential fill error: %s", err)
377 errs = append(errs, err.Error())
388 return nil, errors.New("credential fill errors:\n" + strings.Join(errs, "\n"))
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 {
402 if err := h.Reject(what); err != credHelperNoOp {
407 return errors.New("no valid credential helpers to reject")
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 {
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++ {
427 s.helpers[j].Reject(what)
435 return errors.New("no valid credential helpers to approve")
438 func (s *CredentialHelpers) skip(i int) {
440 s.skippedHelpers[i] = true
444 func (s *CredentialHelpers) skipped(i int) bool {
446 skipped := s.skippedHelpers[i]
451 type nullCredentialHelper struct{}
454 nullCredError = errors.New("No credential helper configured")
455 nullCreds = &nullCredentialHelper{}
458 func (h *nullCredentialHelper) Fill(input Creds) (Creds, error) {
459 return nil, nullCredError
462 func (h *nullCredentialHelper) Approve(creds Creds) error {
466 func (h *nullCredentialHelper) Reject(creds Creds) error {