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 }