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 }