github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+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 }