Merge branch 'master' into tracked-remote
This commit is contained in:
commit
aecf6a0086
@ -65,13 +65,58 @@ func (e *environment) GetAll(key string) []string {
|
||||
return e.Fetcher.GetAll(key)
|
||||
}
|
||||
|
||||
func (e *environment) Bool(key string, def bool) (val bool) {
|
||||
func (e *environment) Bool(key string, def bool) bool {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
if len(s) == 0 {
|
||||
return Bool(s, def)
|
||||
}
|
||||
|
||||
func (e *environment) Int(key string, def int) int {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
return Int(s, def)
|
||||
}
|
||||
|
||||
func (e *environment) All() map[string][]string {
|
||||
return e.Fetcher.All()
|
||||
}
|
||||
|
||||
// Int returns the int value associated with the given value, or the value
|
||||
// "def", if the value is blank.
|
||||
//
|
||||
// To convert from a the string value attached to a given key,
|
||||
// `strconv.Atoi(val)` is called. If `Atoi` returned a non-nil error,
|
||||
// then the value "def" will be returned instead.
|
||||
//
|
||||
// Otherwise, if the value was converted `string -> int` successfully,
|
||||
// then it will be returned wholesale.
|
||||
func Int(value string, def int) int {
|
||||
if len(value) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Bool returns the boolean state associated with the given value, or the
|
||||
// value "def", if the value is blank.
|
||||
//
|
||||
// The "boolean state associated with a given key" is defined as the
|
||||
// case-insensitive string comparison with the following:
|
||||
//
|
||||
// 1) true if...
|
||||
// "true", "1", "on", "yes", or "t"
|
||||
// 2) false if...
|
||||
// "false", "0", "off", "no", "f", or otherwise.
|
||||
func Bool(value string, def bool) bool {
|
||||
if len(value) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "1", "on", "yes", "t":
|
||||
return true
|
||||
case "false", "0", "off", "no", "f":
|
||||
@ -80,21 +125,3 @@ func (e *environment) Bool(key string, def bool) (val bool) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (e *environment) Int(key string, def int) (val int) {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (e *environment) All() map[string][]string {
|
||||
return e.Fetcher.All()
|
||||
}
|
||||
|
@ -50,6 +50,11 @@ func (c *URLConfig) GetAll(prefix, rawurl, key string) []string {
|
||||
return c.git.GetAll(strings.Join([]string{prefix, key}, "."))
|
||||
}
|
||||
|
||||
func (c *URLConfig) Bool(prefix, rawurl, key string, def bool) bool {
|
||||
s, _ := c.Get(prefix, rawurl, key)
|
||||
return Bool(s, def)
|
||||
}
|
||||
|
||||
func (c *URLConfig) getAll(prefix, rawurl, key string) []string {
|
||||
hosts, paths := c.hostsAndPaths(rawurl)
|
||||
|
||||
|
@ -172,8 +172,6 @@ func (o *FilterProcessScanner) Err() error { return o.err }
|
||||
// will read the body of the request. Since the body is _not_ offset, one
|
||||
// request should be read in its entirety before consuming the next request.
|
||||
func (o *FilterProcessScanner) readRequest() (*Request, error) {
|
||||
tracerx.Printf("Read filter-process request.")
|
||||
|
||||
requestList, err := o.pl.readPacketList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -19,8 +19,6 @@ import (
|
||||
func runCatFileBatch(pointerCh chan *WrappedPointer, lockableCh chan string, lockableSet *lockableNameSet, revs *StringChannelWrapper, errCh chan error) error {
|
||||
scanner, err := NewPointerScanner()
|
||||
if err != nil {
|
||||
scanner.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -20,23 +20,11 @@ var (
|
||||
defaultEndpointFinder = NewEndpointFinder(nil)
|
||||
)
|
||||
|
||||
// DoWithAuth sends an HTTP request to get an HTTP response. It attempts to add
|
||||
// authentication from netrc or git's credential helpers if necessary,
|
||||
// supporting basic and ntlm authentication.
|
||||
func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
|
||||
credHelper := c.Credentials
|
||||
if credHelper == nil {
|
||||
credHelper = defaultCredentialHelper
|
||||
}
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
ef := c.Endpoints
|
||||
if ef == nil {
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
apiEndpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, remote, req)
|
||||
apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -94,32 +82,48 @@ func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, cre
|
||||
// 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
|
||||
// This URL is used for the Git Credential Helper. This way existing https
|
||||
// Git remote credentials can be re-used for LFS.
|
||||
func getCreds(credHelper CredentialHelper, netrcFinder NetrcFinder, ef EndpointFinder, remote string, req *http.Request) (Endpoint, Access, Creds, *url.URL, error) {
|
||||
func (c *Client) getCreds(remote string, req *http.Request) (Endpoint, Access, CredentialHelper, *url.URL, Creds, error) {
|
||||
ef := c.Endpoints
|
||||
if ef == nil {
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
operation := getReqOperation(req)
|
||||
apiEndpoint := ef.Endpoint(operation, remote)
|
||||
access := ef.AccessFor(apiEndpoint.Url)
|
||||
|
||||
if access != NTLMAccess {
|
||||
if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
return apiEndpoint, access, nullCreds, nil, nil, nil
|
||||
}
|
||||
|
||||
credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if credsURL == nil {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
return apiEndpoint, access, nullCreds, nil, nil, nil
|
||||
}
|
||||
|
||||
creds, err := fillGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
|
||||
if err == nil {
|
||||
tracerx.Printf("Filled credentials for %s", credsURL)
|
||||
setRequestAuth(req, creds["username"], creds["password"])
|
||||
}
|
||||
return apiEndpoint, access, credHelper, credsURL, creds, err
|
||||
}
|
||||
|
||||
// NTLM ONLY
|
||||
|
||||
credsURL, err := url.Parse(apiEndpoint.Url)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
|
||||
@ -131,20 +135,16 @@ func getCreds(credHelper CredentialHelper, netrcFinder NetrcFinder, ef EndpointF
|
||||
"source": "netrc",
|
||||
}
|
||||
|
||||
return apiEndpoint, access, creds, credsURL, nil
|
||||
return apiEndpoint, access, nullCreds, credsURL, creds, nil
|
||||
}
|
||||
|
||||
creds, err := getGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
// NTLM uses creds to create the session
|
||||
credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
|
||||
return apiEndpoint, access, credHelper, credsURL, creds, err
|
||||
}
|
||||
|
||||
func getGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host, "path": path}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
|
||||
func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (CredentialHelper, Creds, error) {
|
||||
credHelper, input := c.getCredentialHelper(u)
|
||||
creds, err := credHelper.Fill(input)
|
||||
if creds == nil || len(creds) < 1 {
|
||||
errmsg := fmt.Sprintf("Git credentials for %s not found", u)
|
||||
@ -156,17 +156,7 @@ func getGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Reque
|
||||
err = errors.New(errmsg)
|
||||
}
|
||||
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func fillGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
creds, err := getGitCreds(credHelper, ef, req, u)
|
||||
if err == nil {
|
||||
tracerx.Printf("Filled credentials for %s", u)
|
||||
setRequestAuth(req, creds["username"], creds["password"])
|
||||
}
|
||||
|
||||
return creds, err
|
||||
return credHelper, creds, err
|
||||
}
|
||||
|
||||
func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -93,7 +94,6 @@ func TestDoWithAuthApprove(t *testing.T) {
|
||||
assert.True(t, creds.IsApproved(Creds(map[string]string{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"path": "repo/lfs",
|
||||
"protocol": "http",
|
||||
"host": srv.Listener.Addr().String(),
|
||||
})))
|
||||
@ -264,6 +264,51 @@ func TestGetCreds(t *testing.T) {
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic access with usehttppath": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
"credential.usehttppath": "true",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic access with url-specific usehttppath": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
"credential.https://git-server.com.usehttppath": "true",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
@ -295,7 +340,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -369,7 +413,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "user",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -392,7 +435,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -443,7 +485,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -465,7 +506,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "lfs-server.com",
|
||||
"username": "lfs-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -487,7 +527,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com:8080",
|
||||
"username": "git-server.com:8080",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -509,7 +548,6 @@ func TestGetCreds(t *testing.T) {
|
||||
Creds: map[string]string{
|
||||
"host": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
"protocol": "https",
|
||||
"username": "git-server.com",
|
||||
},
|
||||
@ -517,8 +555,6 @@ func TestGetCreds(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
credHelper := &fakeCredentialFiller{}
|
||||
netrcFinder := &fakeNetrc{}
|
||||
for desc, test := range tests {
|
||||
t.Log(desc)
|
||||
req, err := http.NewRequest(test.Method, test.Href, nil)
|
||||
@ -531,8 +567,12 @@ func TestGetCreds(t *testing.T) {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
ef := NewEndpointFinder(NewContext(nil, nil, test.Config))
|
||||
endpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, test.Remote, req)
|
||||
ctx := NewContext(git.NewConfig("", ""), nil, test.Config)
|
||||
client, _ := NewClient(ctx)
|
||||
client.Credentials = &fakeCredentialFiller{}
|
||||
client.Netrc = &fakeNetrc{}
|
||||
client.Endpoints = NewEndpointFinder(ctx)
|
||||
endpoint, access, _, credsURL, creds, err := client.getCreds(test.Remote, req)
|
||||
if !assert.Nil(t, err) {
|
||||
continue
|
||||
}
|
||||
|
@ -79,6 +79,8 @@ func joinURL(prefix, suffix string) string {
|
||||
return prefix + slash + suffix
|
||||
}
|
||||
|
||||
// Do sends an HTTP request to get an HTTP response. It wraps net/http, adding
|
||||
// extra headers, redirection handling, and error reporting.
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
req.Header = c.extraHeadersFor(req)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
435
lfsapi/creds.go
435
lfsapi/creds.go
@ -8,28 +8,34 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// credsConfig supplies configuration options pertaining to the authorization
|
||||
// process in package lfsapi.
|
||||
type credsConfig struct {
|
||||
// AskPass is a string containing an executable name as well as a
|
||||
// program arguments.
|
||||
//
|
||||
// See: https://git-scm.com/docs/gitcredentials#_requesting_credentials
|
||||
// for more.
|
||||
AskPass string `os:"GIT_ASKPASS" git:"core.askpass" os:"SSH_ASKPASS"`
|
||||
// Helper is a string defining the credential helper that Git should use.
|
||||
Helper string `git:"credential.helper"`
|
||||
// Cached is a boolean determining whether or not to enable the
|
||||
// credential cacher.
|
||||
Cached bool
|
||||
// SkipPrompt is a boolean determining whether or not to prompt the user
|
||||
// for a password.
|
||||
SkipPrompt bool `os:"GIT_TERMINAL_PROMPT"`
|
||||
// CredentialHelper is an interface used by the lfsapi Client to interact with
|
||||
// the 'git credential' command: https://git-scm.com/docs/gitcredentials
|
||||
// Other implementations include ASKPASS support, and an in-memory cache.
|
||||
type CredentialHelper interface {
|
||||
Fill(Creds) (Creds, error)
|
||||
Reject(Creds) error
|
||||
Approve(Creds) error
|
||||
}
|
||||
|
||||
// Creds represents a set of key/value pairs that are passed to 'git credential'
|
||||
// as input.
|
||||
type Creds map[string]string
|
||||
|
||||
func bufferCreds(c Creds) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// getCredentialHelper parses a 'credsConfig' from the git and OS environments,
|
||||
@ -37,110 +43,32 @@ type credsConfig struct {
|
||||
//
|
||||
// It returns an error if any configuration was invalid, or otherwise
|
||||
// un-useable.
|
||||
func getCredentialHelper(osEnv, gitEnv config.Environment) (CredentialHelper, error) {
|
||||
ccfg, err := getCredentialConfig(osEnv, gitEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *Client) getCredentialHelper(u *url.URL) (CredentialHelper, Creds) {
|
||||
rawurl := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
if c.uc.Bool("credential", rawurl, "usehttppath", false) {
|
||||
input["path"] = strings.TrimPrefix(u.Path, "/")
|
||||
}
|
||||
|
||||
var hs []CredentialHelper
|
||||
if len(ccfg.Helper) == 0 && len(ccfg.AskPass) > 0 {
|
||||
hs = append(hs, &AskPassCredentialHelper{
|
||||
Program: ccfg.AskPass,
|
||||
})
|
||||
if c.Credentials != nil {
|
||||
return c.Credentials, input
|
||||
}
|
||||
|
||||
var h CredentialHelper
|
||||
h = &commandCredentialHelper{
|
||||
SkipPrompt: ccfg.SkipPrompt,
|
||||
helpers := make([]CredentialHelper, 0, 3)
|
||||
if c.cachingCredHelper != nil {
|
||||
helpers = append(helpers, c.cachingCredHelper)
|
||||
}
|
||||
|
||||
if ccfg.Cached {
|
||||
h = withCredentialCache(h)
|
||||
}
|
||||
hs = append(hs, h)
|
||||
|
||||
switch len(hs) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return hs[0], nil
|
||||
}
|
||||
return CredentialHelpers(hs), nil
|
||||
}
|
||||
|
||||
// getCredentialConfig parses a *credsConfig given the OS and Git
|
||||
// configurations.
|
||||
func getCredentialConfig(o, g config.Environment) (*credsConfig, error) {
|
||||
askpass, ok := o.Get("GIT_ASKPASS")
|
||||
if !ok {
|
||||
askpass, ok = g.Get("core.askpass")
|
||||
}
|
||||
if !ok {
|
||||
askpass, ok = o.Get("SSH_ASKPASS")
|
||||
}
|
||||
helper, _ := g.Get("credential.helper")
|
||||
what := &credsConfig{
|
||||
AskPass: askpass,
|
||||
Helper: helper,
|
||||
Cached: g.Bool("lfs.cachecredentials", true),
|
||||
SkipPrompt: o.Bool("GIT_TERMINAL_PROMPT", false),
|
||||
}
|
||||
|
||||
return what, nil
|
||||
}
|
||||
|
||||
// CredentialHelpers is a []CredentialHelper that iterates through each
|
||||
// credential helper to fill, reject, or approve credentials.
|
||||
type CredentialHelpers []CredentialHelper
|
||||
|
||||
// Fill implements CredentialHelper.Fill by asking each CredentialHelper in
|
||||
// order to fill the credentials.
|
||||
//
|
||||
// If a fill was successful, it is returned immediately, and no other
|
||||
// `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
|
||||
// it is returned immediately.
|
||||
func (h CredentialHelpers) Fill(what Creds) (Creds, error) {
|
||||
for _, c := range h {
|
||||
creds, err := c.Fill(what)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if creds != nil {
|
||||
return creds, nil
|
||||
if c.askpassCredHelper != nil {
|
||||
helper, _ := c.uc.Get("credential", rawurl, "helper")
|
||||
if len(helper) == 0 {
|
||||
helpers = append(helpers, c.askpassCredHelper)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Reject implements CredentialHelper.Reject and rejects the given Creds "what"
|
||||
// amongst all knonw CredentialHelpers. If any `CredentialHelper`s returned a
|
||||
// non-nil error, no further `CredentialHelper`s are notified, so as to prevent
|
||||
// inconsistent state.
|
||||
func (h CredentialHelpers) Reject(what Creds) error {
|
||||
for _, c := range h {
|
||||
if err := c.Reject(what); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Approve implements CredentialHelper.Approve and approves the given Creds
|
||||
// "what" amongst all known CredentialHelpers. If any `CredentialHelper`s
|
||||
// returned a non-nil error, no further `CredentialHelper`s are notified, so as
|
||||
// to prevent inconsistent state.
|
||||
func (h CredentialHelpers) Approve(what Creds) error {
|
||||
for _, c := range h {
|
||||
if err := c.Approve(what); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return NewCredentialHelpers(append(helpers, c.commandCredHelper)), input
|
||||
}
|
||||
|
||||
// AskPassCredentialHelper implements the CredentialHelper type for GIT_ASKPASS
|
||||
@ -234,88 +162,6 @@ func (a *AskPassCredentialHelper) args(prompt string) []string {
|
||||
return []string{prompt}
|
||||
}
|
||||
|
||||
type CredentialHelper interface {
|
||||
Fill(Creds) (Creds, error)
|
||||
Reject(Creds) error
|
||||
Approve(Creds) error
|
||||
}
|
||||
|
||||
type Creds map[string]string
|
||||
|
||||
func bufferCreds(c Creds) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func withCredentialCache(helper CredentialHelper) CredentialHelper {
|
||||
return &credentialCacher{
|
||||
cmu: new(sync.Mutex),
|
||||
creds: make(map[string]Creds),
|
||||
helper: helper,
|
||||
}
|
||||
}
|
||||
|
||||
type credentialCacher struct {
|
||||
// cmu guards creds
|
||||
cmu *sync.Mutex
|
||||
creds map[string]Creds
|
||||
helper CredentialHelper
|
||||
}
|
||||
|
||||
func credCacheKey(creds Creds) string {
|
||||
parts := []string{
|
||||
creds["protocol"],
|
||||
creds["host"],
|
||||
creds["path"],
|
||||
}
|
||||
return strings.Join(parts, "//")
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Fill(creds Creds) (Creds, error) {
|
||||
key := credCacheKey(creds)
|
||||
|
||||
c.cmu.Lock()
|
||||
defer c.cmu.Unlock()
|
||||
|
||||
if cache, ok := c.creds[key]; ok {
|
||||
tracerx.Printf("creds: git credential cache (%q, %q, %q)",
|
||||
creds["protocol"], creds["host"], creds["path"])
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
creds, err := c.helper.Fill(creds)
|
||||
if err == nil && len(creds["username"]) > 0 && len(creds["password"]) > 0 {
|
||||
c.creds[key] = creds
|
||||
}
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Reject(creds Creds) error {
|
||||
c.cmu.Lock()
|
||||
defer c.cmu.Unlock()
|
||||
|
||||
delete(c.creds, credCacheKey(creds))
|
||||
return c.helper.Reject(creds)
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Approve(creds Creds) error {
|
||||
err := c.helper.Approve(creds)
|
||||
if err == nil {
|
||||
c.cmu.Lock()
|
||||
c.creds[credCacheKey(creds)] = creds
|
||||
c.cmu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type commandCredentialHelper struct {
|
||||
SkipPrompt bool
|
||||
}
|
||||
@ -332,6 +178,8 @@ func (h *commandCredentialHelper) Reject(creds Creds) error {
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) Approve(creds Creds) error {
|
||||
tracerx.Printf("creds: git credential approve (%q, %q, %q)",
|
||||
creds["protocol"], creds["host"], creds["path"])
|
||||
_, err := h.exec("approve", creds)
|
||||
return err
|
||||
}
|
||||
@ -383,3 +231,198 @@ func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, e
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
type credentialCacher struct {
|
||||
creds map[string]Creds
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newCredentialCacher() *credentialCacher {
|
||||
return &credentialCacher{creds: make(map[string]Creds)}
|
||||
}
|
||||
|
||||
func credCacheKey(creds Creds) string {
|
||||
parts := []string{
|
||||
creds["protocol"],
|
||||
creds["host"],
|
||||
creds["path"],
|
||||
}
|
||||
return strings.Join(parts, "//")
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Fill(what Creds) (Creds, error) {
|
||||
key := credCacheKey(what)
|
||||
c.mu.Lock()
|
||||
cached, ok := c.creds[key]
|
||||
c.mu.Unlock()
|
||||
|
||||
if ok {
|
||||
tracerx.Printf("creds: git credential cache (%q, %q, %q)",
|
||||
what["protocol"], what["host"], what["path"])
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
return nil, credHelperNoOp
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Approve(what Creds) error {
|
||||
key := credCacheKey(what)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, ok := c.creds[key]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.creds[key] = what
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Reject(what Creds) error {
|
||||
key := credCacheKey(what)
|
||||
c.mu.Lock()
|
||||
delete(c.creds, key)
|
||||
c.mu.Unlock()
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
// CredentialHelpers iterates through a slice of CredentialHelper objects
|
||||
// CredentialHelpers is a []CredentialHelper that iterates through each
|
||||
// credential helper to fill, reject, or approve credentials. Typically, the
|
||||
// first success returns immediately. Errors are reported to tracerx, unless
|
||||
// all credential helpers return errors. Any erroring credential helpers are
|
||||
// skipped for future calls.
|
||||
//
|
||||
// A CredentialHelper can return a credHelperNoOp error, signaling that the
|
||||
// CredentialHelpers should try the next one.
|
||||
type CredentialHelpers struct {
|
||||
helpers []CredentialHelper
|
||||
skippedHelpers map[int]bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewCredentialHelpers initializes a new CredentialHelpers from the given
|
||||
// slice of CredentialHelper instances.
|
||||
func NewCredentialHelpers(helpers []CredentialHelper) CredentialHelper {
|
||||
return &CredentialHelpers{
|
||||
helpers: helpers,
|
||||
skippedHelpers: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
var credHelperNoOp = errors.New("no-op!")
|
||||
|
||||
// Fill implements CredentialHelper.Fill by asking each CredentialHelper in
|
||||
// order to fill the credentials.
|
||||
//
|
||||
// If a fill was successful, it is returned immediately, and no other
|
||||
// `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
|
||||
// it is reported to tracerx, and the next one is attempted. If they all error,
|
||||
// then a collection of all the error messages is returned. Erroring credential
|
||||
// helpers are added to the skip list, and never attempted again for the
|
||||
// lifetime of the current Git LFS command.
|
||||
func (s *CredentialHelpers) Fill(what Creds) (Creds, error) {
|
||||
errs := make([]string, 0, len(s.helpers))
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
creds, err := h.Fill(what)
|
||||
if err != nil {
|
||||
if err != credHelperNoOp {
|
||||
s.skip(i)
|
||||
tracerx.Printf("credential fill error: %s", err)
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if creds != nil {
|
||||
return creds, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.New("credential fill errors:\n" + strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Reject implements CredentialHelper.Reject and rejects the given Creds "what"
|
||||
// with the first successful attempt.
|
||||
func (s *CredentialHelpers) Reject(what Creds) error {
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.Reject(what); err != credHelperNoOp {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("no valid credential helpers to reject")
|
||||
}
|
||||
|
||||
// Approve implements CredentialHelper.Approve and approves the given Creds
|
||||
// "what" with the first successful CredentialHelper. If an error occurrs,
|
||||
// it calls Reject() with the same Creds and returns the error immediately. This
|
||||
// ensures a caching credential helper removes the cache, since the Erroring
|
||||
// CredentialHelper never successfully saved it.
|
||||
func (s *CredentialHelpers) Approve(what Creds) error {
|
||||
skipped := make(map[int]bool)
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
skipped[i] = true
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.Approve(what); err != credHelperNoOp {
|
||||
if err != nil && i > 0 { // clear any cached approvals
|
||||
for j := 0; j < i; j++ {
|
||||
if !skipped[j] {
|
||||
s.helpers[j].Reject(what)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("no valid credential helpers to approve")
|
||||
}
|
||||
|
||||
func (s *CredentialHelpers) skip(i int) {
|
||||
s.mu.Lock()
|
||||
s.skippedHelpers[i] = true
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *CredentialHelpers) skipped(i int) bool {
|
||||
s.mu.Lock()
|
||||
skipped := s.skippedHelpers[i]
|
||||
s.mu.Unlock()
|
||||
return skipped
|
||||
}
|
||||
|
||||
type nullCredentialHelper struct{}
|
||||
|
||||
var (
|
||||
nullCredError = errors.New("No credential helper configured")
|
||||
nullCreds = &nullCredentialHelper{}
|
||||
)
|
||||
|
||||
func (h *nullCredentialHelper) Fill(input Creds) (Creds, error) {
|
||||
return nil, nullCredError
|
||||
}
|
||||
|
||||
func (h *nullCredentialHelper) Approve(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *nullCredentialHelper) Reject(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -5,264 +5,262 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// test that cache satisfies Fill() without looking at creds
|
||||
func TestCredsCacheFillFromCache(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
type testCredHelper struct {
|
||||
fillErr error
|
||||
approveErr error
|
||||
rejectErr error
|
||||
fill []Creds
|
||||
approve []Creds
|
||||
reject []Creds
|
||||
}
|
||||
|
||||
func newTestCredHelper() *testCredHelper {
|
||||
return &testCredHelper{
|
||||
fill: make([]Creds, 0),
|
||||
approve: make([]Creds, 0),
|
||||
reject: make([]Creds, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Fill(input Creds) (Creds, error) {
|
||||
h.fill = append(h.fill, input)
|
||||
return input, h.fillErr
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Approve(creds Creds) error {
|
||||
h.approve = append(h.approve, creds)
|
||||
return h.approveErr
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Reject(creds Creds) error {
|
||||
h.reject = append(h.reject, creds)
|
||||
return h.rejectErr
|
||||
}
|
||||
|
||||
func TestCredHelperSetNoErrors(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
// calling Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
credsWithPass := Creds{"protocol": "https", "host": "example.com", "username": "foo", "password": "bar"}
|
||||
assert.Nil(t, helpers.Approve(credsWithPass))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// calling Approve() again is cached
|
||||
assert.Nil(t, helpers.Approve(credsWithPass))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// access cache
|
||||
for i := 0; i < 3; i++ {
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, credsWithPass, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "u", cached["username"])
|
||||
assert.Equal(t, "p", cached["password"])
|
||||
// Reject() is never cached
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 2, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
// calling Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 3, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
// test that cache caches Fill() value from creds
|
||||
func TestCredsCacheFillFromValidHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
func TestCredHelperSetFillError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
helper1.fillErr = errors.New("boom")
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "u", cached["username"])
|
||||
assert.Equal(t, "p", cached["password"])
|
||||
assert.Nil(t, helpers.Approve(creds))
|
||||
assert.Equal(t, 0, len(helper1.approve))
|
||||
assert.Equal(t, 1, len(helper2.approve))
|
||||
|
||||
creds.list = make([]Creds, 0)
|
||||
filled2, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled2)
|
||||
assert.Equal(t, "u", filled2["username"])
|
||||
assert.Equal(t, "p", filled2["password"])
|
||||
}
|
||||
|
||||
// test that cache ignores Fill() value from creds with missing username+password
|
||||
func TestCredsCacheFillFromInvalidHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "no-password",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "no-password", filled["username"])
|
||||
assert.Equal(t, "", filled["password"])
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
// test that cache ignores Fill() value from creds with error
|
||||
func TestCredsCacheFillFromErroringHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
func TestCredsCacheRejectWithoutError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
// Fill() with cache
|
||||
for i := 0; i < 3; i++ {
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
}
|
||||
|
||||
err := cache.Reject(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 0, len(helper1.reject))
|
||||
assert.Equal(t, 1, len(helper2.reject))
|
||||
|
||||
// Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill)) // still skipped
|
||||
assert.Equal(t, 2, len(helper2.fill))
|
||||
}
|
||||
|
||||
func TestCredsCacheRejectWithError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
func TestCredHelperSetApproveError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
}
|
||||
|
||||
err := cache.Reject(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
func TestCredsCacheApproveWithoutError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
err := cache.Approve(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "U",
|
||||
"password": "P",
|
||||
})
|
||||
approveErr := errors.New("boom")
|
||||
helper1.approveErr = approveErr
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "U", cached["username"])
|
||||
assert.Equal(t, "P", cached["password"])
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, approveErr, helpers.Approve(creds))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// cache is never set
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
}
|
||||
|
||||
func TestCredsCacheApproveWithError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
func TestCredHelperSetFillAndApproveError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
credErr := errors.New("boom")
|
||||
helper1.fillErr = credErr
|
||||
helper2.approveErr = credErr
|
||||
|
||||
err := cache.Approve(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, credErr, helpers.Approve(creds))
|
||||
assert.Equal(t, 0, len(helper1.approve)) // skipped
|
||||
assert.Equal(t, 0, len(helper1.reject)) // skipped
|
||||
assert.Equal(t, 1, len(helper2.approve))
|
||||
|
||||
// never approved, so cache is empty
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill)) // still skipped
|
||||
assert.Equal(t, 2, len(helper2.fill))
|
||||
}
|
||||
|
||||
func newFakeCreds() *fakeCreds {
|
||||
return &fakeCreds{list: make([]Creds, 0)}
|
||||
func TestCredHelperSetRejectError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
rejectErr := errors.New("boom")
|
||||
helper1.rejectErr = rejectErr
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Nil(t, helpers.Approve(creds))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// Fill() with cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, rejectErr, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
// failed Reject() still clears cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
type erroringCreds struct {
|
||||
helper CredentialHelper
|
||||
}
|
||||
func TestCredHelperSetAllFillErrors(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
func (e *erroringCreds) Fill(creds Creds) (Creds, error) {
|
||||
c, _ := e.helper.Fill(creds)
|
||||
return c, errors.New("fill error")
|
||||
}
|
||||
|
||||
func (e *erroringCreds) Reject(creds Creds) error {
|
||||
e.helper.Reject(creds)
|
||||
return errors.New("reject error")
|
||||
}
|
||||
|
||||
func (e *erroringCreds) Approve(creds Creds) error {
|
||||
e.helper.Approve(creds)
|
||||
return errors.New("approve error")
|
||||
}
|
||||
|
||||
type fakeCreds struct {
|
||||
list []Creds
|
||||
}
|
||||
|
||||
func credsMatch(c1, c2 Creds) bool {
|
||||
return c1["protocol"] == c2["protocol"] &&
|
||||
c1["host"] == c2["host"] &&
|
||||
c1["path"] == c2["path"]
|
||||
}
|
||||
|
||||
func (f *fakeCreds) Fill(creds Creds) (Creds, error) {
|
||||
for _, saved := range f.list {
|
||||
if credsMatch(creds, saved) {
|
||||
return saved, nil
|
||||
}
|
||||
helper1.fillErr = errors.New("boom 1")
|
||||
helper2.fillErr = errors.New("boom 2")
|
||||
out, err := helpers.Fill(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "credential fill errors:\nboom 1\nboom 2", err.Error())
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
func (f *fakeCreds) Reject(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
err = helpers.Approve(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "no valid credential helpers to approve", err.Error())
|
||||
}
|
||||
assert.Equal(t, 0, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
func (f *fakeCreds) Approve(creds Creds) error {
|
||||
return nil
|
||||
err = helpers.Reject(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "no valid credential helpers to reject", err.Error())
|
||||
}
|
||||
assert.Equal(t, 0, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
@ -47,10 +45,12 @@ type Client struct {
|
||||
|
||||
LoggingStats bool // DEPRECATED
|
||||
|
||||
// only used for per-host ssl certs
|
||||
gitEnv config.Environment
|
||||
osEnv config.Environment
|
||||
uc *config.URLConfig
|
||||
commandCredHelper *commandCredentialHelper
|
||||
askpassCredHelper *AskPassCredentialHelper
|
||||
cachingCredHelper *credentialCacher
|
||||
gitEnv config.Environment
|
||||
osEnv config.Environment
|
||||
uc *config.URLConfig
|
||||
}
|
||||
|
||||
type Context interface {
|
||||
@ -71,19 +71,14 @@ func NewClient(ctx Context) (*Client, error) {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("bad netrc file %s", netrcfile))
|
||||
}
|
||||
|
||||
creds, err := getCredentialHelper(osEnv, gitEnv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot find credential helper(s)")
|
||||
}
|
||||
|
||||
cacheCreds := gitEnv.Bool("lfs.cachecredentials", true)
|
||||
var sshResolver SSHResolver = &sshAuthClient{os: osEnv}
|
||||
if gitEnv.Bool("lfs.cachecredentials", true) {
|
||||
if cacheCreds {
|
||||
sshResolver = withSSHCache(sshResolver)
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
Endpoints: NewEndpointFinder(ctx),
|
||||
Credentials: creds,
|
||||
SSH: sshResolver,
|
||||
Netrc: netrc,
|
||||
DialTimeout: gitEnv.Int("lfs.dialtimeout", 0),
|
||||
@ -93,9 +88,29 @@ func NewClient(ctx Context) (*Client, error) {
|
||||
SkipSSLVerify: !gitEnv.Bool("http.sslverify", true) || osEnv.Bool("GIT_SSL_NO_VERIFY", false),
|
||||
Verbose: osEnv.Bool("GIT_CURL_VERBOSE", false),
|
||||
DebuggingVerbose: osEnv.Bool("LFS_DEBUG_HTTP", false),
|
||||
gitEnv: gitEnv,
|
||||
osEnv: osEnv,
|
||||
uc: config.NewURLConfig(gitEnv),
|
||||
commandCredHelper: &commandCredentialHelper{
|
||||
SkipPrompt: osEnv.Bool("GIT_TERMINAL_PROMPT", false),
|
||||
},
|
||||
gitEnv: gitEnv,
|
||||
osEnv: osEnv,
|
||||
uc: config.NewURLConfig(gitEnv),
|
||||
}
|
||||
|
||||
askpass, ok := osEnv.Get("GIT_ASKPASS")
|
||||
if !ok {
|
||||
askpass, ok = gitEnv.Get("core.askpass")
|
||||
}
|
||||
if !ok {
|
||||
askpass, _ = osEnv.Get("SSH_ASKPASS")
|
||||
}
|
||||
if len(askpass) > 0 {
|
||||
c.askpassCredHelper = &AskPassCredentialHelper{
|
||||
Program: askpass,
|
||||
}
|
||||
}
|
||||
|
||||
if cacheCreds {
|
||||
c.cachingCredHelper = newCredentialCacher()
|
||||
}
|
||||
|
||||
return c, nil
|
||||
@ -191,34 +206,14 @@ func (e testEnv) GetAll(key string) []string {
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
func (e testEnv) Int(key string, def int) (val int) {
|
||||
func (e testEnv) Int(key string, def int) int {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
return config.Int(s, def)
|
||||
}
|
||||
|
||||
func (e testEnv) Bool(key string, def bool) (val bool) {
|
||||
func (e testEnv) Bool(key string, def bool) bool {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "1", "on", "yes", "t":
|
||||
return true
|
||||
case "false", "0", "off", "no", "f":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return config.Bool(s, def)
|
||||
}
|
||||
|
||||
func (e testEnv) All() map[string][]string {
|
||||
|
@ -90,7 +90,6 @@ func TestNTLMAuth(t *testing.T) {
|
||||
creds := Creds{
|
||||
"protocol": srvURL.Scheme,
|
||||
"host": srvURL.Host,
|
||||
"path": "ntlm",
|
||||
"username": "ntlmdomain\\ntlmuser",
|
||||
"password": "ntlmpass",
|
||||
}
|
||||
|
@ -28,3 +28,25 @@ begin_test "attempt private access without credential helper"
|
||||
grep "Git credentials for $GITSERVER/$reponame not found" push.log
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "askpass: push with bad askpass"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="askpass-with-bad-askpass"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
git lfs track "*.dat"
|
||||
echo "hello" > a.dat
|
||||
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "initial commit"
|
||||
|
||||
git config "credential.helper" ""
|
||||
GIT_TERMINAL_PROMPT=0 GIT_ASKPASS="lfs-askpass-2" SSH_ASKPASS="dont-call-me" GIT_TRACE=1 git push origin master 2>&1 | tee push.log
|
||||
grep "filling with GIT_ASKPASS" push.log # attempt askpass
|
||||
grep 'credential fill error: exec: "lfs-askpass-2"' push.log # askpass fails
|
||||
grep "creds: git credential fill" push.log # attempt git credential
|
||||
)
|
||||
end_test
|
||||
|
@ -4,11 +4,36 @@
|
||||
|
||||
ensure_git_version_isnt $VERSION_LOWER "2.3.0"
|
||||
|
||||
begin_test "credentails with url-specific helper skips askpass"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="url-specific-helper"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
clone_repo "$reponame" "$reponame"
|
||||
git config credential.useHttpPath false
|
||||
git config credential.helper ""
|
||||
git config credential.$GITSERVER.helper "lfstest"
|
||||
|
||||
git lfs track "*.dat"
|
||||
echo "hello" > a.dat
|
||||
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "initial commit"
|
||||
|
||||
# askpass is skipped
|
||||
GIT_ASKPASS="lfs-bad-cmd" GIT_TRACE=1 git push origin master 2>&1 | tee push.log
|
||||
|
||||
[ "0" -eq "$(grep "filling with GIT_ASKPASS" push.log | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials without useHttpPath, with bad path password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="$(basename "$0" ".sh")"
|
||||
reponame="no-httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
@ -20,16 +45,55 @@ begin_test "credentials without useHttpPath, with bad path password"
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
contents="a"
|
||||
contents_oid=$(calc_oid "$contents")
|
||||
|
||||
printf "$contents" > a.dat
|
||||
printf "a" > a.dat
|
||||
git add a.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git push origin without-path 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin without-path 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
|
||||
echo "credential calls have no path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep "no-httppath-bad-password" | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials with url-specific useHttpPath, with bad path password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="url-specific-httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
|
||||
clone_repo "$reponame" with-url-specific-path
|
||||
git config credential.$GITSERVER.useHttpPath false
|
||||
git checkout -b without-path
|
||||
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
printf "a" > a.dat
|
||||
git add a.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
GIT_TRACE=1 git push origin without-path 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -37,7 +101,7 @@ begin_test "credentials with useHttpPath, with wrong password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="$(basename "$0" ".sh")"
|
||||
reponame="httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
@ -56,8 +120,12 @@ begin_test "credentials with useHttpPath, with wrong password"
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git push origin with-path-wrong-pass 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin with-path-wrong-pass 2>&1 | tee push.log
|
||||
[ "0" = "$(grep -c "(1 of 1 files)" push.log)" ]
|
||||
echo "approvals:"
|
||||
[ "0" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "2" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -86,8 +154,17 @@ begin_test "credentials with useHttpPath, with correct password"
|
||||
git add .gitattributes
|
||||
git commit -m "add b.dat"
|
||||
|
||||
git push origin with-path-correct-pass 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin with-path-correct-pass 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
echo "credential calls have path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "$credcalls" | grep "test-credentials" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -175,8 +252,10 @@ begin_test "credentials from netrc"
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git lfs push netrc master 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
echo "any git credential calls:"
|
||||
[ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
|
@ -47,7 +47,6 @@ for typ in "${expiration_types[@]}"; do
|
||||
|
||||
sshurl="${GITSERVER/http:\/\//ssh://git@}/$reponame"
|
||||
git config lfs.url "$sshurl"
|
||||
git config lfs.cachecredentials "true"
|
||||
|
||||
contents="contents"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
|
Loading…
Reference in New Issue
Block a user