git-lfs/lfsapi/auth_test.go
2017-01-06 11:50:24 -07:00

557 lines
15 KiB
Go

package lfsapi
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"github.com/git-lfs/git-lfs/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type authRequest struct {
Test string
}
func TestAuthenticateHeaderAccess(t *testing.T) {
tests := map[string]Access{
"": BasicAccess,
"basic 123": BasicAccess,
"basic": BasicAccess,
"unknown": BasicAccess,
"NTLM": NTLMAccess,
"ntlm": NTLMAccess,
"NTLM 1 2 3": NTLMAccess,
"ntlm 1 2 3": NTLMAccess,
"NEGOTIATE": NTLMAccess,
"negotiate": NTLMAccess,
"NEGOTIATE 1 2 3": NTLMAccess,
"negotiate 1 2 3": NTLMAccess,
}
for _, key := range authenticateHeaders {
for value, expected := range tests {
res := &http.Response{Header: make(http.Header)}
res.Header.Set(key, value)
t.Logf("%s: %s", key, value)
assert.Equal(t, expected, getAuthAccess(res))
}
}
}
func TestDoWithAuthApprove(t *testing.T) {
var called uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
atomic.AddUint32(&called, 1)
assert.Equal(t, "POST", req.Method)
body := &authRequest{}
err := json.NewDecoder(req.Body).Decode(body)
assert.Nil(t, err)
assert.Equal(t, "Approve", body.Test)
w.Header().Set("Lfs-Authenticate", "Basic")
actual := req.Header.Get("Authorization")
if len(actual) == 0 {
w.WriteHeader(http.StatusUnauthorized)
return
}
expected := "Basic " + strings.TrimSpace(
base64.StdEncoding.EncodeToString([]byte("user:pass")),
)
assert.Equal(t, expected, actual)
}))
defer srv.Close()
creds := newMockCredentialHelper()
c, err := NewClient(nil, Env(map[string]string{
"lfs.url": srv.URL + "/repo/lfs",
}))
require.Nil(t, err)
c.Credentials = creds
assert.Equal(t, NoneAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
req, err := http.NewRequest("POST", srv.URL+"/repo/lfs/foo", nil)
require.Nil(t, err)
err = MarshalToRequest(req, &authRequest{Test: "Approve"})
require.Nil(t, err)
res, err := c.DoWithAuth("", req)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.True(t, creds.IsApproved(Creds(map[string]string{
"username": "user",
"password": "pass",
"path": "repo/lfs",
"protocol": "http",
"host": srv.Listener.Addr().String(),
})))
assert.Equal(t, BasicAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
assert.EqualValues(t, 2, called)
}
func TestDoWithAuthReject(t *testing.T) {
var called uint32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
atomic.AddUint32(&called, 1)
assert.Equal(t, "POST", req.Method)
body := &authRequest{}
err := json.NewDecoder(req.Body).Decode(body)
assert.Nil(t, err)
assert.Equal(t, "Reject", body.Test)
actual := req.Header.Get("Authorization")
expected := "Basic " + strings.TrimSpace(
base64.StdEncoding.EncodeToString([]byte("user:pass")),
)
w.Header().Set("Lfs-Authenticate", "Basic")
if actual != expected {
// Write http.StatusUnauthorized to force the credential
// helper to reject the credentials
w.WriteHeader(http.StatusUnauthorized)
} else {
w.WriteHeader(http.StatusOK)
}
}))
defer srv.Close()
invalidCreds := Creds(map[string]string{
"username": "user",
"password": "wrong_pass",
"path": "",
"protocol": "http",
"host": srv.Listener.Addr().String(),
})
creds := newMockCredentialHelper()
creds.Approve(invalidCreds)
assert.True(t, creds.IsApproved(invalidCreds))
c := &Client{
Credentials: creds,
Endpoints: NewEndpointFinder(TestEnv(map[string]string{
"lfs.url": srv.URL,
})),
}
req, err := http.NewRequest("POST", srv.URL, nil)
require.Nil(t, err)
err = MarshalToRequest(req, &authRequest{Test: "Reject"})
require.Nil(t, err)
res, err := c.DoWithAuth("", req)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.False(t, creds.IsApproved(invalidCreds))
assert.True(t, creds.IsApproved(Creds(map[string]string{
"username": "user",
"password": "pass",
"path": "",
"protocol": "http",
"host": srv.Listener.Addr().String(),
})))
assert.EqualValues(t, 3, called)
}
type mockCredentialHelper struct {
Approved map[string]Creds
}
func newMockCredentialHelper() *mockCredentialHelper {
return &mockCredentialHelper{
Approved: make(map[string]Creds),
}
}
func (m *mockCredentialHelper) Fill(input Creds) (Creds, error) {
if found, ok := m.Approved[credsToKey(input)]; ok {
return found, nil
}
output := make(Creds)
for key, value := range input {
output[key] = value
}
if _, ok := output["username"]; !ok {
output["username"] = "user"
}
output["password"] = "pass"
return output, nil
}
func (m *mockCredentialHelper) Approve(creds Creds) error {
m.Approved[credsToKey(creds)] = creds
return nil
}
func (m *mockCredentialHelper) Reject(creds Creds) error {
delete(m.Approved, credsToKey(creds))
return nil
}
func (m *mockCredentialHelper) IsApproved(creds Creds) bool {
if found, ok := m.Approved[credsToKey(creds)]; ok {
return found["password"] == creds["password"]
}
return false
}
func credsToKey(creds Creds) string {
var kvs []string
for _, k := range []string{"protocol", "host", "path"} {
kvs = append(kvs, fmt.Sprintf("%s:%s", k, creds[k]))
}
return strings.Join(kvs, " ")
}
func basicAuth(user, pass string) string {
value := fmt.Sprintf("%s:%s", user, pass)
return fmt.Sprintf("Basic %s", strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(value))))
}
type getCredsExpected struct {
Endpoint string
Access Access
Creds Creds
CredsURL string
Authorization string
}
type getCredsTest struct {
Remote string
Method string
Href string
Header map[string]string
Config map[string]string
Expected getCredsExpected
}
func TestGetCreds(t *testing.T) {
tests := map[string]getCredsTest{
"no access": 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",
},
Expected: getCredsExpected{
Access: NoneAccess,
Endpoint: "https://git-server.com/repo/lfs",
},
},
"basic access": 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",
},
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",
},
},
},
"ntlm": 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": "ntlm",
},
Expected: getCredsExpected{
Access: NTLMAccess,
Endpoint: "https://git-server.com/repo/lfs",
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",
},
},
},
"ntlm with netrc": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://netrc-host.com/repo/lfs/locks",
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,
Endpoint: "https://netrc-host.com/repo/lfs",
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",
Href: "https://git-server.com/repo/lfs/locks",
Header: map[string]string{
"Authorization": "custom",
},
Config: map[string]string{
"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: "custom",
},
},
"netrc": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://netrc-host.com/repo/lfs/locks",
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,
Endpoint: "https://netrc-host.com/repo/lfs",
Authorization: basicAuth("abc", "def"),
},
},
"username in url": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://git-server.com/repo/lfs/locks",
Config: map[string]string{
"lfs.url": "https://user@git-server.com/repo/lfs",
"lfs.https://git-server.com/repo/lfs.access": "basic",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://user@git-server.com/repo/lfs",
Authorization: basicAuth("user", "monkey"),
CredsURL: "https://user@git-server.com/repo/lfs",
Creds: map[string]string{
"protocol": "https",
"host": "git-server.com",
"username": "user",
"password": "monkey",
"path": "repo/lfs",
},
},
},
"different remote url, basic access": 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",
"remote.origin.url": "https://git-server.com/repo",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://git-server.com/repo/lfs",
Authorization: basicAuth("git-server.com", "monkey"),
CredsURL: "https://git-server.com/repo",
Creds: map[string]string{
"protocol": "https",
"host": "git-server.com",
"username": "git-server.com",
"password": "monkey",
"path": "repo",
},
},
},
"api url auth": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://git-server.com/repo/locks",
Config: map[string]string{
"lfs.url": "https://user:pass@git-server.com/repo",
"lfs.https://git-server.com/repo.access": "basic",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://user:pass@git-server.com/repo",
Authorization: basicAuth("user", "pass"),
},
},
"git url auth": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://git-server.com/repo/locks",
Config: map[string]string{
"lfs.url": "https://git-server.com/repo",
"lfs.https://git-server.com/repo.access": "basic",
"remote.origin.url": "https://user:pass@git-server.com/repo",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://git-server.com/repo",
Authorization: basicAuth("user", "pass"),
},
},
"scheme mismatch": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "http://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",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://git-server.com/repo/lfs",
Authorization: basicAuth("git-server.com", "monkey"),
CredsURL: "http://git-server.com/repo/lfs/locks",
Creds: map[string]string{
"protocol": "http",
"host": "git-server.com",
"username": "git-server.com",
"password": "monkey",
"path": "repo/lfs/locks",
},
},
},
"host mismatch": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://lfs-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",
},
Expected: getCredsExpected{
Access: BasicAccess,
Endpoint: "https://git-server.com/repo/lfs",
Authorization: basicAuth("lfs-server.com", "monkey"),
CredsURL: "https://lfs-server.com/repo/lfs/locks",
Creds: map[string]string{
"protocol": "https",
"host": "lfs-server.com",
"username": "lfs-server.com",
"password": "monkey",
"path": "repo/lfs/locks",
},
},
},
"port mismatch": getCredsTest{
Remote: "origin",
Method: "GET",
Href: "https://git-server.com:8080/repo/lfs/locks",
Config: map[string]string{
"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:8080", "monkey"),
CredsURL: "https://git-server.com:8080/repo/lfs/locks",
Creds: map[string]string{
"protocol": "https",
"host": "git-server.com:8080",
"username": "git-server.com:8080",
"password": "monkey",
"path": "repo/lfs/locks",
},
},
},
}
credHelper := &fakeCredentialFiller{}
netrcFinder := &fakeNetrc{}
for desc, test := range tests {
t.Log(desc)
req, err := http.NewRequest(test.Method, test.Href, nil)
if err != nil {
t.Errorf("[%s] %s", desc, err)
continue
}
for key, value := range test.Header {
req.Header.Set(key, value)
}
ef := NewEndpointFinder(TestEnv(test.Config))
endpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, test.Remote, req)
if !assert.Nil(t, err) {
continue
}
assert.Equal(t, test.Expected.Endpoint, endpoint.Url, "endpoint")
assert.Equal(t, test.Expected.Access, access, "access")
assert.Equal(t, test.Expected.Authorization, req.Header.Get("Authorization"), "authorization")
if test.Expected.Creds != nil {
assert.EqualValues(t, test.Expected.Creds, creds)
} else {
assert.Nil(t, creds, "creds")
}
if len(test.Expected.CredsURL) > 0 {
if assert.NotNil(t, credsURL, "credURL") {
assert.Equal(t, test.Expected.CredsURL, credsURL.String(), "credURL")
}
} else {
assert.Nil(t, credsURL)
}
}
}
type fakeCredentialFiller struct{}
func (f *fakeCredentialFiller) Fill(input Creds) (Creds, error) {
output := make(Creds)
for key, value := range input {
output[key] = value
}
if _, ok := output["username"]; !ok {
output["username"] = input["host"]
}
output["password"] = "monkey"
return output, nil
}
func (f *fakeCredentialFiller) Approve(creds Creds) error {
return errors.New("Not implemented")
}
func (f *fakeCredentialFiller) Reject(creds Creds) error {
return errors.New("Not implemented")
}