diff --git a/creds/creds.go b/creds/creds.go index c3604549..23960169 100644 --- a/creds/creds.go +++ b/creds/creds.go @@ -41,6 +41,7 @@ func bufferCreds(c Creds) *bytes.Buffer { } type CredentialHelperContext struct { + netrcCredHelper *netrcCredentialHelper commandCredHelper *commandCredentialHelper askpassCredHelper *AskPassCredentialHelper cachingCredHelper *credentialCacher @@ -51,6 +52,8 @@ type CredentialHelperContext struct { func NewCredentialHelperContext(gitEnv config.Environment, osEnv config.Environment) *CredentialHelperContext { c := &CredentialHelperContext{urlConfig: config.NewURLConfig(gitEnv)} + c.netrcCredHelper = newNetrcCredentialHelper(osEnv) + askpass, ok := osEnv.Get("GIT_ASKPASS") if !ok { askpass, ok = gitEnv.Get("core.askpass") @@ -95,7 +98,10 @@ func (ctxt *CredentialHelperContext) GetCredentialHelper(helper CredentialHelper return helper, input } - helpers := make([]CredentialHelper, 0, 3) + helpers := make([]CredentialHelper, 0, 4) + if ctxt.netrcCredHelper != nil { + helpers = append(helpers, ctxt.netrcCredHelper) + } if ctxt.cachingCredHelper != nil { helpers = append(helpers, ctxt.cachingCredHelper) } diff --git a/lfsapi/lfsapi_nix.go b/creds/creds_nix.go similarity index 76% rename from lfsapi/lfsapi_nix.go rename to creds/creds_nix.go index a3eb87cb..5a4f01b4 100644 --- a/lfsapi/lfsapi_nix.go +++ b/creds/creds_nix.go @@ -1,5 +1,5 @@ // +build !windows -package lfsapi +package creds var netrcBasename = ".netrc" diff --git a/lfsapi/lfsapi_windows.go b/creds/creds_windows.go similarity index 76% rename from lfsapi/lfsapi_windows.go rename to creds/creds_windows.go index ff8ea0ba..6c9576de 100644 --- a/lfsapi/lfsapi_windows.go +++ b/creds/creds_windows.go @@ -1,5 +1,5 @@ // +build windows -package lfsapi +package creds var netrcBasename = "_netrc" diff --git a/creds/netrc.go b/creds/netrc.go new file mode 100644 index 00000000..0395ccb8 --- /dev/null +++ b/creds/netrc.go @@ -0,0 +1,130 @@ +package creds + +import ( + "net" + "os" + "path/filepath" + "strings" + + "github.com/git-lfs/git-lfs/config" + "github.com/git-lfs/go-netrc/netrc" + "github.com/rubyist/tracerx" +) + +type NetrcFinder interface { + FindMachine(string) *netrc.Machine +} + +func ParseNetrc(osEnv config.Environment) (NetrcFinder, string, error) { + home, _ := osEnv.Get("HOME") + if len(home) == 0 { + return &noFinder{}, "", nil + } + + nrcfilename := filepath.Join(home, netrcBasename) + if _, err := os.Stat(nrcfilename); err != nil { + return &noFinder{}, nrcfilename, nil + } + + f, err := netrc.ParseFile(nrcfilename) + return f, nrcfilename, err +} + +type noFinder struct{} + +func (f *noFinder) FindMachine(host string) *netrc.Machine { + return nil +} + +// NetrcCredentialHelper retrieves credentials from a .netrc file +type netrcCredentialHelper struct { + netrcFinder NetrcFinder + skip map[string]bool +} + +var defaultNetrcFinder = &noFinder{} + +// NewNetrcCredentialHelper creates a new netrc credential helper using a +// .netrc file gleaned from the OS environment +func newNetrcCredentialHelper(osEnv config.Environment) *netrcCredentialHelper { + netrcFinder, netrcfile, err := ParseNetrc(osEnv) + if err != nil { + tracerx.Printf("bad netrc file %s: %s", netrcfile, err) + return nil + } + + if netrcFinder == nil { + netrcFinder = defaultNetrcFinder + } + + return &netrcCredentialHelper{netrcFinder: netrcFinder, skip: make(map[string]bool)} +} + +func (c *netrcCredentialHelper) Fill(what Creds) (Creds, error) { + host, err := getNetrcHostname(what["host"]) + if err != nil { + return nil, credHelperNoOp + } + + if c.skip[host] { + return nil, credHelperNoOp + } + + if machine := c.netrcFinder.FindMachine(host); machine != nil { + creds := make(Creds) + creds["username"] = machine.Login + creds["password"] = machine.Password + creds["protocol"] = what["protocol"] + creds["host"] = what["host"] + creds["scheme"] = what["scheme"] + creds["path"] = what["path"] + creds["source"] = "netrc" + tracerx.Printf("netrc: git credential fill (%q, %q, %q)", + what["protocol"], what["host"], what["path"]) + return creds, nil + } + + return nil, credHelperNoOp +} + +func getNetrcHostname(hostname string) (string, error) { + if strings.Contains(hostname, ":") { + host, _, err := net.SplitHostPort(hostname) + if err != nil { + tracerx.Printf("netrc: error parsing %q: %s", hostname, err) + return "", err + } + return host, nil + } + + return hostname, nil +} + +func (c *netrcCredentialHelper) Approve(what Creds) error { + if what["source"] == "netrc" { + host, err := getNetrcHostname(what["host"]) + if err != nil { + return credHelperNoOp + } + tracerx.Printf("netrc: git credential approve (%q, %q, %q)", + what["protocol"], what["host"], what["path"]) + c.skip[host] = false + return nil + } + return credHelperNoOp +} + +func (c *netrcCredentialHelper) Reject(what Creds) error { + if what["source"] == "netrc" { + host, err := getNetrcHostname(what["host"]) + if err != nil { + return credHelperNoOp + } + + tracerx.Printf("netrc: git credential reject (%q, %q, %q)", + what["protocol"], what["host"], what["path"]) + c.skip[host] = true + return nil + } + return credHelperNoOp +} diff --git a/creds/netrc_test.go b/creds/netrc_test.go new file mode 100644 index 00000000..80bdbb8d --- /dev/null +++ b/creds/netrc_test.go @@ -0,0 +1,82 @@ +package creds + +import ( + "strings" + "testing" + + "github.com/git-lfs/go-netrc/netrc" +) + +func TestNetrcWithHostAndPort(t *testing.T) { + var netrcHelper netrcCredentialHelper + netrcHelper.netrcFinder = &fakeNetrc{} + + what := make(Creds) + what["protocol"] = "http" + what["host"] = "netrc-host:123" + what["path"] = "/foo/bar" + + creds, err := netrcHelper.Fill(what) + if err != nil { + t.Fatalf("error retrieving netrc credentials: %s", err) + } + + username := creds["username"] + if username != "abc" { + t.Fatalf("bad username: %s", username) + } + + password := creds["password"] + if password != "def" { + t.Fatalf("bad password: %s", password) + } +} + +func TestNetrcWithHost(t *testing.T) { + var netrcHelper netrcCredentialHelper + netrcHelper.netrcFinder = &fakeNetrc{} + + what := make(Creds) + what["protocol"] = "http" + what["host"] = "netrc-host" + what["path"] = "/foo/bar" + + creds, err := netrcHelper.Fill(what) + if err != nil { + t.Fatalf("error retrieving netrc credentials: %s", err) + } + + username := creds["username"] + if username != "abc" { + t.Fatalf("bad username: %s", username) + } + + password := creds["password"] + if password != "def" { + t.Fatalf("bad password: %s", password) + } +} + +func TestNetrcWithBadHost(t *testing.T) { + var netrcHelper netrcCredentialHelper + netrcHelper.netrcFinder = &fakeNetrc{} + + what := make(Creds) + what["protocol"] = "http" + what["host"] = "other-host" + what["path"] = "/foo/bar" + + _, err := netrcHelper.Fill(what) + if err != credHelperNoOp { + t.Fatalf("expected no-op for unknown host other-host") + } +} + +type fakeNetrc struct{} + +func (n *fakeNetrc) FindMachine(host string) *netrc.Machine { + if strings.Contains(host, "netrc") { + return &netrc.Machine{Login: "abc", Password: "def"} + } + return nil +} diff --git a/lfsapi/auth.go b/lfsapi/auth.go index 35557239..ed121c79 100644 --- a/lfsapi/auth.go +++ b/lfsapi/auth.go @@ -3,7 +3,6 @@ package lfsapi import ( "encoding/base64" "fmt" - "net" "net/http" "net/url" "os" @@ -12,12 +11,10 @@ import ( "github.com/git-lfs/git-lfs/creds" "github.com/git-lfs/git-lfs/errors" "github.com/git-lfs/git-lfs/lfshttp" - "github.com/git-lfs/go-netrc/netrc" "github.com/rubyist/tracerx" ) var ( - defaultNetrcFinder = &noFinder{} defaultEndpointFinder = NewEndpointFinder(nil) ) @@ -134,16 +131,11 @@ func (c *Client) getCreds(remote string, access Access, req *http.Request) (cred ef = defaultEndpointFinder } - netrcFinder := c.Netrc - if netrcFinder == nil { - netrcFinder = defaultNetrcFinder - } - operation := getReqOperation(req) apiEndpoint := ef.Endpoint(operation, remote) if access.Mode() != NTLMAccess { - if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access.Mode() == NoneAccess { + if requestHasAuth(req) || access.Mode() == NoneAccess { return creds.NullCreds, nil, nil, nil } @@ -171,18 +163,6 @@ func (c *Client) getCreds(remote string, access Access, req *http.Request) (cred return creds.NullCreds, nil, nil, errors.Wrap(err, "creds") } - if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil { - cred := creds.Creds{ - "protocol": credsURL.Scheme, - "host": credsURL.Host, - "username": netrcMachine.Login, - "password": netrcMachine.Password, - "source": "netrc", - } - - return creds.NullCreds, credsURL, cred, nil - } - // NTLM uses creds to create the session credHelper, creds, err := c.getGitCreds(ef, req, credsURL) return credHelper, credsURL, creds, err @@ -204,33 +184,6 @@ func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) ( return credHelper, creds, err } -func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine { - hostname := req.URL.Host - var host string - - if strings.Contains(hostname, ":") { - var err error - host, _, err = net.SplitHostPort(hostname) - if err != nil { - tracerx.Printf("netrc: error parsing %q: %s", hostname, err) - return nil - } - } else { - host = hostname - } - - return netrcFinder.FindMachine(host) -} - -func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool { - if machine := getAuthFromNetrc(netrcFinder, req); machine != nil { - setRequestAuth(req, machine.Login, machine.Password) - return true - } - - return false -} - func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint lfshttp.Endpoint, req *http.Request) (*url.URL, error) { apiURL, err := url.Parse(apiEndpoint.Url) if err != nil { diff --git a/lfsapi/auth_test.go b/lfsapi/auth_test.go index a873a873..d2d259ec 100644 --- a/lfsapi/auth_test.go +++ b/lfsapi/auth_test.go @@ -448,27 +448,6 @@ func TestGetCreds(t *testing.T) { }, }, }, - "ntlm with netrc": getCredsTest{ - Remote: "origin", - Method: "GET", - Href: "https://netrc-host.com/repo/lfs/locks", - Endpoint: "https://netrc-host.com/repo/lfs", - Config: map[string]string{ - "lfs.url": "https://netrc-host.com/repo/lfs", - "lfs.https://netrc-host.com/repo/lfs.access": "ntlm", - }, - Expected: getCredsExpected{ - Access: NTLMAccess, - CredsURL: "https://netrc-host.com/repo/lfs", - Creds: map[string]string{ - "protocol": "https", - "host": "netrc-host.com", - "username": "abc", - "password": "def", - "source": "netrc", - }, - }, - }, "custom auth": getCredsTest{ Remote: "origin", Method: "GET", @@ -486,20 +465,6 @@ func TestGetCreds(t *testing.T) { Authorization: "custom", }, }, - "netrc": getCredsTest{ - Remote: "origin", - Method: "GET", - Href: "https://netrc-host.com/repo/lfs/locks", - Endpoint: "https://netrc-host.com/repo/lfs", - Config: map[string]string{ - "lfs.url": "https://netrc-host.com/repo/lfs", - "lfs.https://netrc-host.com/repo/lfs.access": "basic", - }, - Expected: getCredsExpected{ - Access: BasicAccess, - Authorization: basicAuth("abc", "def"), - }, - }, "username in url": getCredsTest{ Remote: "origin", Method: "GET", @@ -675,7 +640,6 @@ func TestGetCreds(t *testing.T) { ctx := lfshttp.NewContext(git.NewConfig("", ""), nil, test.Config) client, _ := NewClient(ctx) client.Credentials = &fakeCredentialFiller{} - client.Netrc = &fakeNetrc{} client.Endpoints = NewEndpointFinder(ctx) _, credsURL, creds, err := client.getCreds(test.Remote, client.Endpoints.AccessFor(test.Endpoint), req) if !assert.Nil(t, err) { diff --git a/lfsapi/lfsapi.go b/lfsapi/lfsapi.go index a5d09f84..722f37b6 100644 --- a/lfsapi/lfsapi.go +++ b/lfsapi/lfsapi.go @@ -13,7 +13,6 @@ import ( type Client struct { Endpoints EndpointFinder Credentials creds.CredentialHelper - Netrc NetrcFinder ntlmSessions map[string]ntlm.ClientSession ntlmMu sync.Mutex @@ -30,10 +29,6 @@ func NewClient(ctx lfshttp.Context) (*Client, error) { gitEnv := ctx.GitEnv() osEnv := ctx.OSEnv() - netrc, netrcfile, err := ParseNetrc(osEnv) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("bad netrc file %s", netrcfile)) - } httpClient, err := lfshttp.NewClient(ctx) if err != nil { @@ -42,7 +37,6 @@ func NewClient(ctx lfshttp.Context) (*Client, error) { c := &Client{ Endpoints: NewEndpointFinder(ctx), - Netrc: netrc, client: httpClient, credContext: creds.NewCredentialHelperContext(gitEnv, osEnv), } diff --git a/lfsapi/netrc.go b/lfsapi/netrc.go deleted file mode 100644 index 3d2c6555..00000000 --- a/lfsapi/netrc.go +++ /dev/null @@ -1,34 +0,0 @@ -package lfsapi - -import ( - "os" - "path/filepath" - - "github.com/git-lfs/git-lfs/config" - "github.com/git-lfs/go-netrc/netrc" -) - -type NetrcFinder interface { - FindMachine(string) *netrc.Machine -} - -func ParseNetrc(osEnv config.Environment) (NetrcFinder, string, error) { - home, _ := osEnv.Get("HOME") - if len(home) == 0 { - return &noFinder{}, "", nil - } - - nrcfilename := filepath.Join(home, netrcBasename) - if _, err := os.Stat(nrcfilename); err != nil { - return &noFinder{}, nrcfilename, nil - } - - f, err := netrc.ParseFile(nrcfilename) - return f, nrcfilename, err -} - -type noFinder struct{} - -func (f *noFinder) FindMachine(host string) *netrc.Machine { - return nil -} diff --git a/lfsapi/netrc_test.go b/lfsapi/netrc_test.go deleted file mode 100644 index 88f495dc..00000000 --- a/lfsapi/netrc_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package lfsapi - -import ( - "net/http" - "net/url" - "strings" - "testing" - - "github.com/git-lfs/go-netrc/netrc" -) - -func TestNetrcWithHostAndPort(t *testing.T) { - netrcFinder := &fakeNetrc{} - u, err := url.Parse("http://netrc-host:123/foo/bar") - if err != nil { - t.Fatal(err) - } - - req := &http.Request{ - URL: u, - Header: http.Header{}, - } - - if !setAuthFromNetrc(netrcFinder, req) { - t.Fatal("no netrc match") - } - - auth := req.Header.Get("Authorization") - if auth != "Basic YWJjOmRlZg==" { - t.Fatalf("bad basic auth: %q", auth) - } -} - -func TestNetrcWithHost(t *testing.T) { - netrcFinder := &fakeNetrc{} - u, err := url.Parse("http://netrc-host/foo/bar") - if err != nil { - t.Fatal(err) - } - - req := &http.Request{ - URL: u, - Header: http.Header{}, - } - - if !setAuthFromNetrc(netrcFinder, req) { - t.Fatalf("no netrc match") - } - - auth := req.Header.Get("Authorization") - if auth != "Basic YWJjOmRlZg==" { - t.Fatalf("bad basic auth: %q", auth) - } -} - -func TestNetrcWithBadHost(t *testing.T) { - netrcFinder := &fakeNetrc{} - u, err := url.Parse("http://other-host/foo/bar") - if err != nil { - t.Fatal(err) - } - - req := &http.Request{ - URL: u, - Header: http.Header{}, - } - - if setAuthFromNetrc(netrcFinder, req) { - t.Fatalf("unexpected netrc match") - } - - auth := req.Header.Get("Authorization") - if auth != "" { - t.Fatalf("bad basic auth: %q", auth) - } -} - -type fakeNetrc struct{} - -func (n *fakeNetrc) FindMachine(host string) *netrc.Machine { - if strings.Contains(host, "netrc") { - return &netrc.Machine{Login: "abc", Password: "def"} - } - return nil -} diff --git a/t/t-credentials.sh b/t/t-credentials.sh index 3ae6a50a..8879b2db 100755 --- a/t/t-credentials.sh +++ b/t/t-credentials.sh @@ -255,8 +255,14 @@ begin_test "credentials from netrc" GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log grep "Uploading LFS objects: 100% (1/1), 7 B" push.log - echo "any git credential calls:" - [ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ] + echo "any netrc credential calls:" + [ "4" -eq "$(cat push.log | grep "netrc: git credential" | wc -l)" ] + + echo "any netrc credential fills:" + [ "2" -eq "$(cat push.log | grep "netrc: git credential fill" | wc -l)" ] + + echo "any netrc credential approvals:" + [ "2" -eq "$(cat push.log | grep "netrc: git credential approve" | wc -l)" ] ) end_test @@ -288,8 +294,14 @@ begin_test "credentials from netrc with unknown keyword" GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log grep "Uploading LFS objects: 100% (1/1), 7 B" push.log - echo "any git credential calls:" - [ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ] + echo "any netrc credential calls:" + [ "4" -eq "$(cat push.log | grep "netrc: git credential" | wc -l)" ] + + echo "any netrc credential fills:" + [ "2" -eq "$(cat push.log | grep "netrc: git credential fill" | wc -l)" ] + + echo "any netrc credential approvals:" + [ "2" -eq "$(cat push.log | grep "netrc: git credential approve" | wc -l)" ] ) end_test