creds: Add new NetrcCredentialHelper

This commit adds a new credential helper, NetrcCredentialHelper,
to retrieve credentials from a .netrc file. This replaces the
old .netrc authorization behaviour, which was done directly from
the `doWithAuth()` code.

Additionally, this commit moves all of the `.netrc` functionality
out of `lfsapi` and into `creds`. This was done both because
`creds` is now the logical place for the `.netrc` functionality,
and to prevent an import cycle between `creds` and `lfsapi`.
This commit is contained in:
Preben Ingvaldsen 2018-10-08 15:47:17 -07:00
parent e7b6b33b5a
commit 1026ff073c
11 changed files with 238 additions and 216 deletions

@ -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)
}

@ -1,5 +1,5 @@
// +build !windows
package lfsapi
package creds
var netrcBasename = ".netrc"

@ -1,5 +1,5 @@
// +build windows
package lfsapi
package creds
var netrcBasename = "_netrc"

130
creds/netrc.go Normal file

@ -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
}

82
creds/netrc_test.go Normal file

@ -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
}

@ -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 {

@ -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) {

@ -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),
}

@ -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
}

@ -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
}

@ -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