github.com/git-lfs/git-lfs@v2.5.2+incompatible/lfsapi/auth_test.go (about)

     1  package lfsapi
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"sync/atomic"
    11  	"testing"
    12  
    13  	"github.com/git-lfs/git-lfs/errors"
    14  	"github.com/git-lfs/git-lfs/git"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  type authRequest struct {
    20  	Test string
    21  }
    22  
    23  func TestAuthenticateHeaderAccess(t *testing.T) {
    24  	tests := map[string]Access{
    25  		"":                BasicAccess,
    26  		"basic 123":       BasicAccess,
    27  		"basic":           BasicAccess,
    28  		"unknown":         BasicAccess,
    29  		"NTLM":            NTLMAccess,
    30  		"ntlm":            NTLMAccess,
    31  		"NTLM 1 2 3":      NTLMAccess,
    32  		"ntlm 1 2 3":      NTLMAccess,
    33  		"NEGOTIATE":       NTLMAccess,
    34  		"negotiate":       NTLMAccess,
    35  		"NEGOTIATE 1 2 3": NTLMAccess,
    36  		"negotiate 1 2 3": NTLMAccess,
    37  	}
    38  
    39  	for _, key := range authenticateHeaders {
    40  		for value, expected := range tests {
    41  			res := &http.Response{Header: make(http.Header)}
    42  			res.Header.Set(key, value)
    43  			t.Logf("%s: %s", key, value)
    44  			assert.Equal(t, expected, getAuthAccess(res))
    45  		}
    46  	}
    47  }
    48  
    49  func TestDoWithAuthApprove(t *testing.T) {
    50  	var called uint32
    51  
    52  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    53  		atomic.AddUint32(&called, 1)
    54  		assert.Equal(t, "POST", req.Method)
    55  
    56  		body := &authRequest{}
    57  		err := json.NewDecoder(req.Body).Decode(body)
    58  		assert.Nil(t, err)
    59  		assert.Equal(t, "Approve", body.Test)
    60  
    61  		w.Header().Set("Lfs-Authenticate", "Basic")
    62  		actual := req.Header.Get("Authorization")
    63  		if len(actual) == 0 {
    64  			w.WriteHeader(http.StatusUnauthorized)
    65  			return
    66  		}
    67  
    68  		expected := "Basic " + strings.TrimSpace(
    69  			base64.StdEncoding.EncodeToString([]byte("user:pass")),
    70  		)
    71  		assert.Equal(t, expected, actual)
    72  	}))
    73  	defer srv.Close()
    74  
    75  	creds := newMockCredentialHelper()
    76  	c, err := NewClient(NewContext(nil, nil, map[string]string{
    77  		"lfs.url": srv.URL + "/repo/lfs",
    78  	}))
    79  	require.Nil(t, err)
    80  	c.Credentials = creds
    81  
    82  	assert.Equal(t, NoneAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
    83  
    84  	req, err := http.NewRequest("POST", srv.URL+"/repo/lfs/foo", nil)
    85  	require.Nil(t, err)
    86  
    87  	err = MarshalToRequest(req, &authRequest{Test: "Approve"})
    88  	require.Nil(t, err)
    89  
    90  	res, err := c.DoWithAuth("", req)
    91  	require.Nil(t, err)
    92  
    93  	assert.Equal(t, http.StatusOK, res.StatusCode)
    94  	assert.True(t, creds.IsApproved(Creds(map[string]string{
    95  		"username": "user",
    96  		"password": "pass",
    97  		"protocol": "http",
    98  		"host":     srv.Listener.Addr().String(),
    99  	})))
   100  	assert.Equal(t, BasicAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
   101  	assert.EqualValues(t, 2, called)
   102  }
   103  
   104  func TestDoWithAuthReject(t *testing.T) {
   105  	var called uint32
   106  
   107  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   108  		atomic.AddUint32(&called, 1)
   109  		assert.Equal(t, "POST", req.Method)
   110  
   111  		body := &authRequest{}
   112  		err := json.NewDecoder(req.Body).Decode(body)
   113  		assert.Nil(t, err)
   114  		assert.Equal(t, "Reject", body.Test)
   115  
   116  		actual := req.Header.Get("Authorization")
   117  		expected := "Basic " + strings.TrimSpace(
   118  			base64.StdEncoding.EncodeToString([]byte("user:pass")),
   119  		)
   120  
   121  		w.Header().Set("Lfs-Authenticate", "Basic")
   122  		if actual != expected {
   123  			// Write http.StatusUnauthorized to force the credential
   124  			// helper to reject the credentials
   125  			w.WriteHeader(http.StatusUnauthorized)
   126  		} else {
   127  			w.WriteHeader(http.StatusOK)
   128  		}
   129  	}))
   130  	defer srv.Close()
   131  
   132  	invalidCreds := Creds(map[string]string{
   133  		"username": "user",
   134  		"password": "wrong_pass",
   135  		"path":     "",
   136  		"protocol": "http",
   137  		"host":     srv.Listener.Addr().String(),
   138  	})
   139  
   140  	creds := newMockCredentialHelper()
   141  	creds.Approve(invalidCreds)
   142  	assert.True(t, creds.IsApproved(invalidCreds))
   143  
   144  	c, _ := NewClient(nil)
   145  	c.Credentials = creds
   146  	c.Endpoints = NewEndpointFinder(NewContext(nil, nil, map[string]string{
   147  		"lfs.url": srv.URL,
   148  	}))
   149  
   150  	req, err := http.NewRequest("POST", srv.URL, nil)
   151  	require.Nil(t, err)
   152  
   153  	err = MarshalToRequest(req, &authRequest{Test: "Reject"})
   154  	require.Nil(t, err)
   155  
   156  	res, err := c.DoWithAuth("", req)
   157  	require.Nil(t, err)
   158  
   159  	assert.Equal(t, http.StatusOK, res.StatusCode)
   160  	assert.False(t, creds.IsApproved(invalidCreds))
   161  	assert.True(t, creds.IsApproved(Creds(map[string]string{
   162  		"username": "user",
   163  		"password": "pass",
   164  		"path":     "",
   165  		"protocol": "http",
   166  		"host":     srv.Listener.Addr().String(),
   167  	})))
   168  	assert.EqualValues(t, 3, called)
   169  }
   170  
   171  type mockCredentialHelper struct {
   172  	Approved map[string]Creds
   173  }
   174  
   175  func newMockCredentialHelper() *mockCredentialHelper {
   176  	return &mockCredentialHelper{
   177  		Approved: make(map[string]Creds),
   178  	}
   179  }
   180  
   181  func (m *mockCredentialHelper) Fill(input Creds) (Creds, error) {
   182  	if found, ok := m.Approved[credsToKey(input)]; ok {
   183  		return found, nil
   184  	}
   185  
   186  	output := make(Creds)
   187  	for key, value := range input {
   188  		output[key] = value
   189  	}
   190  	if _, ok := output["username"]; !ok {
   191  		output["username"] = "user"
   192  	}
   193  	output["password"] = "pass"
   194  	return output, nil
   195  }
   196  
   197  func (m *mockCredentialHelper) Approve(creds Creds) error {
   198  	m.Approved[credsToKey(creds)] = creds
   199  	return nil
   200  }
   201  
   202  func (m *mockCredentialHelper) Reject(creds Creds) error {
   203  	delete(m.Approved, credsToKey(creds))
   204  	return nil
   205  }
   206  
   207  func (m *mockCredentialHelper) IsApproved(creds Creds) bool {
   208  	if found, ok := m.Approved[credsToKey(creds)]; ok {
   209  		return found["password"] == creds["password"]
   210  	}
   211  	return false
   212  }
   213  
   214  func credsToKey(creds Creds) string {
   215  	var kvs []string
   216  	for _, k := range []string{"protocol", "host", "path"} {
   217  		kvs = append(kvs, fmt.Sprintf("%s:%s", k, creds[k]))
   218  	}
   219  
   220  	return strings.Join(kvs, " ")
   221  }
   222  
   223  func basicAuth(user, pass string) string {
   224  	value := fmt.Sprintf("%s:%s", user, pass)
   225  	return fmt.Sprintf("Basic %s", strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(value))))
   226  }
   227  
   228  type getCredsExpected struct {
   229  	Endpoint      string
   230  	Access        Access
   231  	Creds         Creds
   232  	CredsURL      string
   233  	Authorization string
   234  }
   235  
   236  type getCredsTest struct {
   237  	Remote   string
   238  	Method   string
   239  	Href     string
   240  	Header   map[string]string
   241  	Config   map[string]string
   242  	Expected getCredsExpected
   243  }
   244  
   245  func TestGetCreds(t *testing.T) {
   246  	tests := map[string]getCredsTest{
   247  		"no access": getCredsTest{
   248  			Remote: "origin",
   249  			Method: "GET",
   250  			Href:   "https://git-server.com/repo/lfs/locks",
   251  			Config: map[string]string{
   252  				"lfs.url": "https://git-server.com/repo/lfs",
   253  			},
   254  			Expected: getCredsExpected{
   255  				Access:   NoneAccess,
   256  				Endpoint: "https://git-server.com/repo/lfs",
   257  			},
   258  		},
   259  		"basic access": getCredsTest{
   260  			Remote: "origin",
   261  			Method: "GET",
   262  			Href:   "https://git-server.com/repo/lfs/locks",
   263  			Config: map[string]string{
   264  				"lfs.url": "https://git-server.com/repo/lfs",
   265  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   266  			},
   267  			Expected: getCredsExpected{
   268  				Access:        BasicAccess,
   269  				Endpoint:      "https://git-server.com/repo/lfs",
   270  				Authorization: basicAuth("git-server.com", "monkey"),
   271  				CredsURL:      "https://git-server.com/repo/lfs",
   272  				Creds: map[string]string{
   273  					"protocol": "https",
   274  					"host":     "git-server.com",
   275  					"username": "git-server.com",
   276  					"password": "monkey",
   277  				},
   278  			},
   279  		},
   280  		"basic access with usehttppath": getCredsTest{
   281  			Remote: "origin",
   282  			Method: "GET",
   283  			Href:   "https://git-server.com/repo/lfs/locks",
   284  			Config: map[string]string{
   285  				"lfs.url": "https://git-server.com/repo/lfs",
   286  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   287  				"credential.usehttppath":                     "true",
   288  			},
   289  			Expected: getCredsExpected{
   290  				Access:        BasicAccess,
   291  				Endpoint:      "https://git-server.com/repo/lfs",
   292  				Authorization: basicAuth("git-server.com", "monkey"),
   293  				CredsURL:      "https://git-server.com/repo/lfs",
   294  				Creds: map[string]string{
   295  					"protocol": "https",
   296  					"host":     "git-server.com",
   297  					"username": "git-server.com",
   298  					"password": "monkey",
   299  					"path":     "repo/lfs",
   300  				},
   301  			},
   302  		},
   303  		"basic access with url-specific usehttppath": getCredsTest{
   304  			Remote: "origin",
   305  			Method: "GET",
   306  			Href:   "https://git-server.com/repo/lfs/locks",
   307  			Config: map[string]string{
   308  				"lfs.url": "https://git-server.com/repo/lfs",
   309  				"lfs.https://git-server.com/repo/lfs.access":    "basic",
   310  				"credential.https://git-server.com.usehttppath": "true",
   311  			},
   312  			Expected: getCredsExpected{
   313  				Access:        BasicAccess,
   314  				Endpoint:      "https://git-server.com/repo/lfs",
   315  				Authorization: basicAuth("git-server.com", "monkey"),
   316  				CredsURL:      "https://git-server.com/repo/lfs",
   317  				Creds: map[string]string{
   318  					"protocol": "https",
   319  					"host":     "git-server.com",
   320  					"username": "git-server.com",
   321  					"password": "monkey",
   322  					"path":     "repo/lfs",
   323  				},
   324  			},
   325  		},
   326  		"ntlm": getCredsTest{
   327  			Remote: "origin",
   328  			Method: "GET",
   329  			Href:   "https://git-server.com/repo/lfs/locks",
   330  			Config: map[string]string{
   331  				"lfs.url": "https://git-server.com/repo/lfs",
   332  				"lfs.https://git-server.com/repo/lfs.access": "ntlm",
   333  			},
   334  			Expected: getCredsExpected{
   335  				Access:   NTLMAccess,
   336  				Endpoint: "https://git-server.com/repo/lfs",
   337  				CredsURL: "https://git-server.com/repo/lfs",
   338  				Creds: map[string]string{
   339  					"protocol": "https",
   340  					"host":     "git-server.com",
   341  					"username": "git-server.com",
   342  					"password": "monkey",
   343  				},
   344  			},
   345  		},
   346  		"ntlm with netrc": getCredsTest{
   347  			Remote: "origin",
   348  			Method: "GET",
   349  			Href:   "https://netrc-host.com/repo/lfs/locks",
   350  			Config: map[string]string{
   351  				"lfs.url": "https://netrc-host.com/repo/lfs",
   352  				"lfs.https://netrc-host.com/repo/lfs.access": "ntlm",
   353  			},
   354  			Expected: getCredsExpected{
   355  				Access:   NTLMAccess,
   356  				Endpoint: "https://netrc-host.com/repo/lfs",
   357  				CredsURL: "https://netrc-host.com/repo/lfs",
   358  				Creds: map[string]string{
   359  					"protocol": "https",
   360  					"host":     "netrc-host.com",
   361  					"username": "abc",
   362  					"password": "def",
   363  					"source":   "netrc",
   364  				},
   365  			},
   366  		},
   367  		"custom auth": getCredsTest{
   368  			Remote: "origin",
   369  			Method: "GET",
   370  			Href:   "https://git-server.com/repo/lfs/locks",
   371  			Header: map[string]string{
   372  				"Authorization": "custom",
   373  			},
   374  			Config: map[string]string{
   375  				"lfs.url": "https://git-server.com/repo/lfs",
   376  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   377  			},
   378  			Expected: getCredsExpected{
   379  				Access:        BasicAccess,
   380  				Endpoint:      "https://git-server.com/repo/lfs",
   381  				Authorization: "custom",
   382  			},
   383  		},
   384  		"netrc": getCredsTest{
   385  			Remote: "origin",
   386  			Method: "GET",
   387  			Href:   "https://netrc-host.com/repo/lfs/locks",
   388  			Config: map[string]string{
   389  				"lfs.url": "https://netrc-host.com/repo/lfs",
   390  				"lfs.https://netrc-host.com/repo/lfs.access": "basic",
   391  			},
   392  			Expected: getCredsExpected{
   393  				Access:        BasicAccess,
   394  				Endpoint:      "https://netrc-host.com/repo/lfs",
   395  				Authorization: basicAuth("abc", "def"),
   396  			},
   397  		},
   398  		"username in url": getCredsTest{
   399  			Remote: "origin",
   400  			Method: "GET",
   401  			Href:   "https://git-server.com/repo/lfs/locks",
   402  			Config: map[string]string{
   403  				"lfs.url": "https://user@git-server.com/repo/lfs",
   404  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   405  			},
   406  			Expected: getCredsExpected{
   407  				Access:        BasicAccess,
   408  				Endpoint:      "https://user@git-server.com/repo/lfs",
   409  				Authorization: basicAuth("user", "monkey"),
   410  				CredsURL:      "https://user@git-server.com/repo/lfs",
   411  				Creds: map[string]string{
   412  					"protocol": "https",
   413  					"host":     "git-server.com",
   414  					"username": "user",
   415  					"password": "monkey",
   416  				},
   417  			},
   418  		},
   419  		"different remote url, basic access": getCredsTest{
   420  			Remote: "origin",
   421  			Method: "GET",
   422  			Href:   "https://git-server.com/repo/lfs/locks",
   423  			Config: map[string]string{
   424  				"lfs.url": "https://git-server.com/repo/lfs",
   425  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   426  				"remote.origin.url":                          "https://git-server.com/repo",
   427  			},
   428  			Expected: getCredsExpected{
   429  				Access:        BasicAccess,
   430  				Endpoint:      "https://git-server.com/repo/lfs",
   431  				Authorization: basicAuth("git-server.com", "monkey"),
   432  				CredsURL:      "https://git-server.com/repo",
   433  				Creds: map[string]string{
   434  					"protocol": "https",
   435  					"host":     "git-server.com",
   436  					"username": "git-server.com",
   437  					"password": "monkey",
   438  				},
   439  			},
   440  		},
   441  		"api url auth": getCredsTest{
   442  			Remote: "origin",
   443  			Method: "GET",
   444  			Href:   "https://git-server.com/repo/locks",
   445  			Config: map[string]string{
   446  				"lfs.url": "https://user:pass@git-server.com/repo",
   447  				"lfs.https://git-server.com/repo.access": "basic",
   448  			},
   449  			Expected: getCredsExpected{
   450  				Access:        BasicAccess,
   451  				Endpoint:      "https://user:pass@git-server.com/repo",
   452  				Authorization: basicAuth("user", "pass"),
   453  			},
   454  		},
   455  		"git url auth": getCredsTest{
   456  			Remote: "origin",
   457  			Method: "GET",
   458  			Href:   "https://git-server.com/repo/locks",
   459  			Config: map[string]string{
   460  				"lfs.url": "https://git-server.com/repo",
   461  				"lfs.https://git-server.com/repo.access": "basic",
   462  				"remote.origin.url":                      "https://user:pass@git-server.com/repo",
   463  			},
   464  			Expected: getCredsExpected{
   465  				Access:        BasicAccess,
   466  				Endpoint:      "https://git-server.com/repo",
   467  				Authorization: basicAuth("user", "pass"),
   468  			},
   469  		},
   470  		"scheme mismatch": getCredsTest{
   471  			Remote: "origin",
   472  			Method: "GET",
   473  			Href:   "http://git-server.com/repo/lfs/locks",
   474  			Config: map[string]string{
   475  				"lfs.url": "https://git-server.com/repo/lfs",
   476  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   477  			},
   478  			Expected: getCredsExpected{
   479  				Access:        BasicAccess,
   480  				Endpoint:      "https://git-server.com/repo/lfs",
   481  				Authorization: basicAuth("git-server.com", "monkey"),
   482  				CredsURL:      "http://git-server.com/repo/lfs/locks",
   483  				Creds: map[string]string{
   484  					"protocol": "http",
   485  					"host":     "git-server.com",
   486  					"username": "git-server.com",
   487  					"password": "monkey",
   488  				},
   489  			},
   490  		},
   491  		"host mismatch": getCredsTest{
   492  			Remote: "origin",
   493  			Method: "GET",
   494  			Href:   "https://lfs-server.com/repo/lfs/locks",
   495  			Config: map[string]string{
   496  				"lfs.url": "https://git-server.com/repo/lfs",
   497  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   498  			},
   499  			Expected: getCredsExpected{
   500  				Access:        BasicAccess,
   501  				Endpoint:      "https://git-server.com/repo/lfs",
   502  				Authorization: basicAuth("lfs-server.com", "monkey"),
   503  				CredsURL:      "https://lfs-server.com/repo/lfs/locks",
   504  				Creds: map[string]string{
   505  					"protocol": "https",
   506  					"host":     "lfs-server.com",
   507  					"username": "lfs-server.com",
   508  					"password": "monkey",
   509  				},
   510  			},
   511  		},
   512  		"port mismatch": getCredsTest{
   513  			Remote: "origin",
   514  			Method: "GET",
   515  			Href:   "https://git-server.com:8080/repo/lfs/locks",
   516  			Config: map[string]string{
   517  				"lfs.url": "https://git-server.com/repo/lfs",
   518  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   519  			},
   520  			Expected: getCredsExpected{
   521  				Access:        BasicAccess,
   522  				Endpoint:      "https://git-server.com/repo/lfs",
   523  				Authorization: basicAuth("git-server.com:8080", "monkey"),
   524  				CredsURL:      "https://git-server.com:8080/repo/lfs/locks",
   525  				Creds: map[string]string{
   526  					"protocol": "https",
   527  					"host":     "git-server.com:8080",
   528  					"username": "git-server.com:8080",
   529  					"password": "monkey",
   530  				},
   531  			},
   532  		},
   533  		"bare ssh URI": getCredsTest{
   534  			Remote: "origin",
   535  			Method: "POST",
   536  			Href:   "https://git-server.com/repo/lfs/objects/batch",
   537  			Config: map[string]string{
   538  				"lfs.url": "https://git-server.com/repo/lfs",
   539  				"lfs.https://git-server.com/repo/lfs.access": "basic",
   540  
   541  				"remote.origin.url": "git@git-server.com:repo.git",
   542  			},
   543  			Expected: getCredsExpected{
   544  				Access:        BasicAccess,
   545  				Endpoint:      "https://git-server.com/repo/lfs",
   546  				Authorization: basicAuth("git-server.com", "monkey"),
   547  				CredsURL:      "https://git-server.com/repo/lfs",
   548  				Creds: map[string]string{
   549  					"host":     "git-server.com",
   550  					"password": "monkey",
   551  					"protocol": "https",
   552  					"username": "git-server.com",
   553  				},
   554  			},
   555  		},
   556  	}
   557  
   558  	for desc, test := range tests {
   559  		t.Log(desc)
   560  		req, err := http.NewRequest(test.Method, test.Href, nil)
   561  		if err != nil {
   562  			t.Errorf("[%s] %s", desc, err)
   563  			continue
   564  		}
   565  
   566  		for key, value := range test.Header {
   567  			req.Header.Set(key, value)
   568  		}
   569  
   570  		ctx := NewContext(git.NewConfig("", ""), nil, test.Config)
   571  		client, _ := NewClient(ctx)
   572  		client.Credentials = &fakeCredentialFiller{}
   573  		client.Netrc = &fakeNetrc{}
   574  		client.Endpoints = NewEndpointFinder(ctx)
   575  		endpoint, access, _, credsURL, creds, err := client.getCreds(test.Remote, req)
   576  		if !assert.Nil(t, err) {
   577  			continue
   578  		}
   579  		assert.Equal(t, test.Expected.Endpoint, endpoint.Url, "endpoint")
   580  		assert.Equal(t, test.Expected.Access, access, "access")
   581  		assert.Equal(t, test.Expected.Authorization, req.Header.Get("Authorization"), "authorization")
   582  
   583  		if test.Expected.Creds != nil {
   584  			assert.EqualValues(t, test.Expected.Creds, creds)
   585  		} else {
   586  			assert.Nil(t, creds, "creds")
   587  		}
   588  
   589  		if len(test.Expected.CredsURL) > 0 {
   590  			if assert.NotNil(t, credsURL, "credURL") {
   591  				assert.Equal(t, test.Expected.CredsURL, credsURL.String(), "credURL")
   592  			}
   593  		} else {
   594  			assert.Nil(t, credsURL)
   595  		}
   596  	}
   597  }
   598  
   599  type fakeCredentialFiller struct{}
   600  
   601  func (f *fakeCredentialFiller) Fill(input Creds) (Creds, error) {
   602  	output := make(Creds)
   603  	for key, value := range input {
   604  		output[key] = value
   605  	}
   606  	if _, ok := output["username"]; !ok {
   607  		output["username"] = input["host"]
   608  	}
   609  	output["password"] = "monkey"
   610  	return output, nil
   611  }
   612  
   613  func (f *fakeCredentialFiller) Approve(creds Creds) error {
   614  	return errors.New("Not implemented")
   615  }
   616  
   617  func (f *fakeCredentialFiller) Reject(creds Creds) error {
   618  	return errors.New("Not implemented")
   619  }