11 "github.com/git-lfs/git-lfs/config"
12 "github.com/git-lfs/git-lfs/errors"
13 "github.com/rubyist/tracerx"
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
22 // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials
24 AskPass string `os:"GIT_ASKPASS" git:"core.askpass" os:"SSH_ASKPASS"`
25 // Cached is a boolean determining whether or not to enable the
27 Cached bool `git:"lfs.cachecredentials"`
28 // SkipPrompt is a boolean determining whether or not to prompt the user
30 SkipPrompt bool `os:"GIT_TERMINAL_PROMPT"`
33 // getCredentialHelper parses a 'credsConfig' from the git and OS environments,
34 // returning the appropriate CredentialHelper to authenticate requests with.
36 // It returns an error if any configuration was invalid, or otherwise
38 func getCredentialHelper(cfg *config.Configuration) (CredentialHelper, error) {
39 ccfg, err := getCredentialConfig(cfg)
44 var hs []CredentialHelper
45 if len(ccfg.AskPass) > 0 {
46 hs = append(hs, &AskPassCredentialHelper{
47 Program: ccfg.AskPass,
51 var h CredentialHelper
52 h = &commandCredentialHelper{
53 SkipPrompt: ccfg.SkipPrompt,
57 h = withCredentialCache(h)
67 return CredentialHelpers(hs), nil
70 // getCredentialConfig parses a *credsConfig given the OS and Git
72 func getCredentialConfig(cfg *config.Configuration) (*credsConfig, error) {
75 if err := cfg.Unmarshal(&what); err != nil {
81 // CredentialHelpers is a []CredentialHelper that iterates through each
82 // credential helper to fill, reject, or approve credentials.
83 type CredentialHelpers []CredentialHelper
85 // Fill implements CredentialHelper.Fill by asking each CredentialHelper in
86 // order to fill the credentials.
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) {
93 creds, err := c.Fill(what)
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 {
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 {
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.
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".
144 // It accepts the password as coming from the program's stdout, as when invoked
145 // with the given arguments (see (*AskPassCredentialHelper).args() below)./
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
155 Scheme: what["protocol"],
160 // 'ucmd' will run the GIT_ASKPASS (or core.askpass) command prompting
162 ucmd := exec.Command(a.Program, a.args(fmt.Sprintf("Username for %q", u))...)
166 tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(ucmd.Args, " "))
167 if err := ucmd.Run(); err != nil {
172 return nil, errors.New(err.String())
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
178 u.User = url.User(username)
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))...)
187 tracerx.Printf("creds: filling with GIT_ASKPASS: %s", strings.Join(pcmd.Args, " "))
188 if err := pcmd.Run(); err != nil {
193 return nil, errors.New(err.String())
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.
199 creds["username"] = strings.TrimSpace(user.String())
200 creds["password"] = strings.TrimSpace(pass.String())
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 }
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 }
213 // args returns the arguments given to the ASKPASS program, if a prompt was
216 // See: https://git-scm.com/docs/gitcredentials#_requesting_credentials for
218 func (a *AskPassCredentialHelper) args(prompt string) []string {
219 if len(prompt) == 0 {
222 return []string{prompt}
225 type CredentialHelper interface {
226 Fill(Creds) (Creds, error)
231 type Creds map[string]string
233 func bufferCreds(c Creds) *bytes.Buffer {
234 buf := new(bytes.Buffer)
236 for k, v := range c {
238 buf.Write([]byte("="))
240 buf.Write([]byte("\n"))
246 func withCredentialCache(helper CredentialHelper) CredentialHelper {
247 return &credentialCacher{
248 cmu: new(sync.Mutex),
249 creds: make(map[string]Creds),
254 type credentialCacher struct {
257 creds map[string]Creds
258 helper CredentialHelper
261 func credCacheKey(creds Creds) string {
267 return strings.Join(parts, "//")
270 func (c *credentialCacher) Fill(creds Creds) (Creds, error) {
271 key := credCacheKey(creds)
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"])
282 creds, err := c.helper.Fill(creds)
283 if err == nil && len(creds["username"]) > 0 && len(creds["password"]) > 0 {
289 func (c *credentialCacher) Reject(creds Creds) error {
293 delete(c.creds, credCacheKey(creds))
294 return c.helper.Reject(creds)
297 func (c *credentialCacher) Approve(creds Creds) error {
298 err := c.helper.Approve(creds)
301 c.creds[credCacheKey(creds)] = creds
307 type commandCredentialHelper struct {
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)
317 func (h *commandCredentialHelper) Reject(creds Creds) error {
318 _, err := h.exec("reject", creds)
322 func (h *commandCredentialHelper) Approve(creds Creds) error {
323 _, err := h.exec("approve", creds)
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)
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.
338 See https://github.com/git-lfs/git-lfs/issues/117 for more details.
346 if _, ok := err.(*exec.ExitError); ok {
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"])
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" {
360 return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
364 for _, line := range strings.Split(output.String(), "\n") {
365 pieces := strings.SplitN(line, "=", 2)
366 if len(pieces) < 2 || len(pieces[1]) < 1 {
369 creds[pieces[0]] = pieces[1]