721c0668c013d5c445da8d8a377429c722567c84
[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/config"
12         "github.com/git-lfs/git-lfs/errors"
13         "github.com/rubyist/tracerx"
14 )
15
16 // credsConfig supplies configuration options pertaining to the authorization
17 // process in package lfsapi.
18 type credsConfig struct {
19         // AskPass is a string containing an executable name as well as a
20         // program arguments.
21         //
22         // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials
23         // for more.
24         AskPass string `os:"GIT_ASKPASS" git:"core.askpass" os:"SSH_ASKPASS"`
25         // Cached is a boolean determining whether or not to enable the
26         // credential cacher.
27         Cached bool `git:"lfs.cachecredentials"`
28         // SkipPrompt is a boolean determining whether or not to prompt the user
29         // for a password.
30         SkipPrompt bool `os:"GIT_TERMINAL_PROMPT"`
31 }
32
33 // getCredentialHelper parses a 'credsConfig' from the git and OS environments,
34 // returning the appropriate CredentialHelper to authenticate requests with.
35 //
36 // It returns an error if any configuration was invalid, or otherwise
37 // un-useable.
38 func getCredentialHelper(cfg *config.Configuration) (CredentialHelper, error) {
39         ccfg, err := getCredentialConfig(cfg)
40         if err != nil {
41                 return nil, err
42         }
43
44         var hs []CredentialHelper
45         if len(ccfg.AskPass) > 0 {
46                 hs = append(hs, &AskPassCredentialHelper{
47                         Program: ccfg.AskPass,
48                 })
49         }
50
51         var h CredentialHelper
52         h = &commandCredentialHelper{
53                 SkipPrompt: ccfg.SkipPrompt,
54         }
55
56         if ccfg.Cached {
57                 h = withCredentialCache(h)
58         }
59         hs = append(hs, h)
60
61         switch len(hs) {
62         case 0:
63                 return nil, nil
64         case 1:
65                 return hs[0], nil
66         }
67         return CredentialHelpers(hs), nil
68 }
69
70 // getCredentialConfig parses a *credsConfig given the OS and Git
71 // configurations.
72 func getCredentialConfig(cfg *config.Configuration) (*credsConfig, error) {
73         var what credsConfig
74
75         if err := cfg.Unmarshal(&what); err != nil {
76                 return nil, err
77         }
78         return &what, nil
79 }
80
81 // CredentialHelpers is a []CredentialHelper that iterates through each
82 // credential helper to fill, reject, or approve credentials.
83 type CredentialHelpers []CredentialHelper
84
85 // Fill implements CredentialHelper.Fill by asking each CredentialHelper in
86 // order to fill the credentials.
87 //
88 // If a fill was successful, it is returned immediately, and no other
89 // `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
90 // it is returned immediately.
91 func (h CredentialHelpers) Fill(what Creds) (Creds, error) {
92         for _, c := range h {
93                 creds, err := c.Fill(what)
94                 if err != nil {
95                         return nil, err
96                 }
97
98                 if creds != nil {
99                         return creds, nil
100                 }
101         }
102
103         return nil, nil
104 }
105
106 // Reject implements CredentialHelper.Reject and rejects the given Creds "what"
107 // amongst all knonw CredentialHelpers. If any `CredentialHelper`s returned a
108 // non-nil error, no further `CredentialHelper`s are notified, so as to prevent
109 // inconsistent state.
110 func (h CredentialHelpers) Reject(what Creds) error {
111         for _, c := range h {
112                 if err := c.Reject(what); err != nil {
113                         return err
114                 }
115         }
116
117         return nil
118 }
119
120 // Approve implements CredentialHelper.Approve and approves the given Creds
121 // "what" amongst all knonw CredentialHelpers. If any `CredentialHelper`s
122 // returned a non-nil error, no further `CredentialHelper`s are notified, so as
123 // to prevent inconsistent state.
124 func (h CredentialHelpers) Approve(what Creds) error {
125         for _, c := range h {
126                 if err := c.Approve(what); err != nil {
127                         return err
128                 }
129         }
130
131         return nil
132 }
133
134 // AskPassCredentialHelper implements the CredentialHelper type for GIT_ASKPASS
135 // and 'core.askpass' configuration values.
136 type AskPassCredentialHelper struct {
137         // Program is the executable program's absolute or relative name.
138         Program string
139 }
140
141 // Fill implements fill by running the ASKPASS program and returning its output
142 // as a password encoded in the Creds type given the key "password".
143 //
144 // It accepts the password as coming from the program's stdout, as when invoked
145 // with the given arguments (see (*AskPassCredentialHelper).args() below)./
146 //
147 // If there was an error running the command, it is returned instead of a set of
148 // filled credentials.
149 func (a *AskPassCredentialHelper) Fill(what Creds) (Creds, error) {
150         var user bytes.Buffer
151         var pass bytes.Buffer
152         var err bytes.Buffer
153
154         u := &url.URL{
155                 Scheme: what["protocol"],
156                 Host:   what["host"],
157                 Path:   what["path"],
158         }
159
160         // 'ucmd' will run the GIT_ASKPASS (or core.askpass) command prompting
161         // for a username.
162         ucmd := exec.Command(a.Program, a.args(fmt.Sprintf("Username for %q", u))...)
163         ucmd.Stderr = &err
164         ucmd.Stdout = &user
165
166         tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(ucmd.Args, " "))
167         if err := ucmd.Run(); err != nil {
168                 return nil, err
169         }
170
171         if err.Len() > 0 {
172                 return nil, errors.New(err.String())
173         }
174
175         if username := strings.TrimSpace(user.String()); len(username) > 0 {
176                 // If a non-empty username was given, add it to the URL via func
177                 // 'net/url.User()'.
178                 u.User = url.User(username)
179         }
180
181         // Regardless, create 'pcmd' to run the GIT_ASKPASS (or core.askpass)
182         // command prompting for a password.
183         pcmd := exec.Command(a.Program, a.args(fmt.Sprintf("Password for %q", u))...)
184         pcmd.Stderr = &err
185         pcmd.Stdout = &pass
186
187         tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(pcmd.Args, " "))
188         if err := pcmd.Run(); err != nil {
189                 return nil, err
190         }
191
192         if err.Len() > 0 {
193                 return nil, errors.New(err.String())
194         }
195
196         // Finally, now that we have the username and password information,
197         // store it in the creds instance that we will return to the caller.
198         creds := make(Creds)
199         creds["username"] = strings.TrimSpace(user.String())
200         creds["password"] = strings.TrimSpace(pass.String())
201
202         return creds, nil
203 }
204
205 // Approve implements CredentialHelper.Approve, and returns nil. The ASKPASS
206 // credential helper does not implement credential approval.
207 func (a *AskPassCredentialHelper) Approve(_ Creds) error { return nil }
208
209 // Reject implements CredentialHelper.Reject, and returns nil. The ASKPASS
210 // credential helper does not implement credential rejection.
211 func (a *AskPassCredentialHelper) Reject(_ Creds) error { return nil }
212
213 // args returns the arguments given to the ASKPASS program, if a prompt was
214 // given.
215
216 // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
217 // more.
218 func (a *AskPassCredentialHelper) args(prompt string) []string {
219         if len(prompt) == 0 {
220                 return nil
221         }
222         return []string{prompt}
223 }
224
225 type CredentialHelper interface {
226         Fill(Creds) (Creds, error)
227         Reject(Creds) error
228         Approve(Creds) error
229 }
230
231 type Creds map[string]string
232
233 func bufferCreds(c Creds) *bytes.Buffer {
234         buf := new(bytes.Buffer)
235
236         for k, v := range c {
237                 buf.Write([]byte(k))
238                 buf.Write([]byte("="))
239                 buf.Write([]byte(v))
240                 buf.Write([]byte("\n"))
241         }
242
243         return buf
244 }
245
246 func withCredentialCache(helper CredentialHelper) CredentialHelper {
247         return &credentialCacher{
248                 cmu:    new(sync.Mutex),
249                 creds:  make(map[string]Creds),
250                 helper: helper,
251         }
252 }
253
254 type credentialCacher struct {
255         // cmu guards creds
256         cmu    *sync.Mutex
257         creds  map[string]Creds
258         helper CredentialHelper
259 }
260
261 func credCacheKey(creds Creds) string {
262         parts := []string{
263                 creds["protocol"],
264                 creds["host"],
265                 creds["path"],
266         }
267         return strings.Join(parts, "//")
268 }
269
270 func (c *credentialCacher) Fill(creds Creds) (Creds, error) {
271         key := credCacheKey(creds)
272
273         c.cmu.Lock()
274         defer c.cmu.Unlock()
275
276         if cache, ok := c.creds[key]; ok {
277                 tracerx.Printf("creds: git credential cache (%q, %q, %q)",
278                         creds["protocol"], creds["host"], creds["path"])
279                 return cache, nil
280         }
281
282         creds, err := c.helper.Fill(creds)
283         if err == nil && len(creds["username"]) > 0 && len(creds["password"]) > 0 {
284                 c.creds[key] = creds
285         }
286         return creds, err
287 }
288
289 func (c *credentialCacher) Reject(creds Creds) error {
290         c.cmu.Lock()
291         defer c.cmu.Unlock()
292
293         delete(c.creds, credCacheKey(creds))
294         return c.helper.Reject(creds)
295 }
296
297 func (c *credentialCacher) Approve(creds Creds) error {
298         err := c.helper.Approve(creds)
299         if err == nil {
300                 c.cmu.Lock()
301                 c.creds[credCacheKey(creds)] = creds
302                 c.cmu.Unlock()
303         }
304         return err
305 }
306
307 type commandCredentialHelper struct {
308         SkipPrompt bool
309 }
310
311 func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
312         tracerx.Printf("creds: git credential fill (%q, %q, %q)",
313                 creds["protocol"], creds["host"], creds["path"])
314         return h.exec("fill", creds)
315 }
316
317 func (h *commandCredentialHelper) Reject(creds Creds) error {
318         _, err := h.exec("reject", creds)
319         return err
320 }
321
322 func (h *commandCredentialHelper) Approve(creds Creds) error {
323         _, err := h.exec("approve", creds)
324         return err
325 }
326
327 func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, error) {
328         output := new(bytes.Buffer)
329         cmd := exec.Command("git", "credential", subcommand)
330         cmd.Stdin = bufferCreds(input)
331         cmd.Stdout = output
332         /*
333            There is a reason we don't hook up stderr here:
334            Git's credential cache daemon helper does not close its stderr, so if this
335            process is the process that fires up the daemon, it will wait forever
336            (until the daemon exits, really) trying to read from stderr.
337
338            See https://github.com/git-lfs/git-lfs/issues/117 for more details.
339         */
340
341         err := cmd.Start()
342         if err == nil {
343                 err = cmd.Wait()
344         }
345
346         if _, ok := err.(*exec.ExitError); ok {
347                 if h.SkipPrompt {
348                         return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
349                                 input["protocol"], input["host"])
350                 }
351
352                 // 'git credential' exits with 128 if the helper doesn't fill the username
353                 // and password values.
354                 if subcommand == "fill" && err.Error() == "exit status 128" {
355                         return nil, nil
356                 }
357         }
358
359         if err != nil {
360                 return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
361         }
362
363         creds := make(Creds)
364         for _, line := range strings.Split(output.String(), "\n") {
365                 pieces := strings.SplitN(line, "=", 2)
366                 if len(pieces) < 2 || len(pieces[1]) < 1 {
367                         continue
368                 }
369                 creds[pieces[0]] = pieces[1]
370         }
371
372         return creds, nil
373 }