Imported Upstream version 2.5.0
[scm/test.git] / lfsapi / creds.go
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 }