1 // Tests for Keep HTTP handlers:
7 // The HTTP handlers are responsible for enforcing permission policy,
8 // so these tests must exercise all possible permission permutations.
16 "github.com/gorilla/mux"
26 // A RequestTester represents the parameters for an HTTP request to
27 // be issued on behalf of a unit test.
28 type RequestTester struct {
35 // Test GetBlockHandler on the following situations:
36 // - permissions off, unauthenticated request, unsigned locator
37 // - permissions on, authenticated request, signed locator
38 // - permissions on, authenticated request, unsigned locator
39 // - permissions on, unauthenticated request, signed locator
40 // - permissions on, authenticated request, expired locator
42 func TestGetHandler(t *testing.T) {
45 // Prepare two test Keep volumes. Our block is stored on the second volume.
46 KeepVM = MakeTestVolumeManager(2)
49 vols := KeepVM.Volumes()
50 if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil {
54 // Set up a REST router for testing the handlers.
55 rest := MakeRESTRouter()
57 // Create locators for testing.
58 // Turn on permission settings so we can generate signed locators.
59 enforce_permissions = true
60 PermissionSecret = []byte(known_key)
61 permission_ttl = time.Duration(300) * time.Second
64 unsigned_locator = "/" + TEST_HASH
65 valid_timestamp = time.Now().Add(permission_ttl)
66 expired_timestamp = time.Now().Add(-time.Hour)
67 signed_locator = "/" + SignLocator(TEST_HASH, known_token, valid_timestamp)
68 expired_locator = "/" + SignLocator(TEST_HASH, known_token, expired_timestamp)
72 // Test unauthenticated request with permissions off.
73 enforce_permissions = false
75 // Unauthenticated request, unsigned locator
77 response := IssueRequest(rest,
80 uri: unsigned_locator,
83 "Unauthenticated request, unsigned locator", http.StatusOK, response)
85 "Unauthenticated request, unsigned locator",
88 received_xbs := response.Header().Get("X-Block-Size")
89 expected_xbs := fmt.Sprintf("%d", len(TEST_BLOCK))
90 if received_xbs != expected_xbs {
91 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
96 enforce_permissions = true
98 // Authenticated request, signed locator
100 response = IssueRequest(rest, &RequestTester{
103 api_token: known_token,
106 "Authenticated request, signed locator", http.StatusOK, response)
108 "Authenticated request, signed locator", string(TEST_BLOCK), response)
109 received_xbs = response.Header().Get("X-Block-Size")
110 expected_xbs = fmt.Sprintf("%d", len(TEST_BLOCK))
111 if received_xbs != expected_xbs {
112 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
115 // Authenticated request, unsigned locator
116 // => PermissionError
117 response = IssueRequest(rest, &RequestTester{
119 uri: unsigned_locator,
120 api_token: known_token,
122 ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
124 // Unauthenticated request, signed locator
125 // => PermissionError
126 response = IssueRequest(rest, &RequestTester{
131 "Unauthenticated request, signed locator",
132 PermissionError.HTTPCode, response)
134 // Authenticated request, expired locator
136 response = IssueRequest(rest, &RequestTester{
138 uri: expired_locator,
139 api_token: known_token,
142 "Authenticated request, expired locator",
143 ExpiredError.HTTPCode, response)
146 // Test PutBlockHandler on the following situations:
148 // - with server key, authenticated request, unsigned locator
149 // - with server key, unauthenticated request, unsigned locator
151 func TestPutHandler(t *testing.T) {
154 // Prepare two test Keep volumes.
155 KeepVM = MakeTestVolumeManager(2)
158 // Set up a REST router for testing the handlers.
159 rest := MakeRESTRouter()
164 // Unauthenticated request, no server key
165 // => OK (unsigned response)
166 unsigned_locator := "/" + TEST_HASH
167 response := IssueRequest(rest,
170 uri: unsigned_locator,
171 request_body: TEST_BLOCK,
175 "Unauthenticated request, no server key", http.StatusOK, response)
177 "Unauthenticated request, no server key",
178 TEST_HASH_PUT_RESPONSE, response)
180 // ------------------
181 // With a server key.
183 PermissionSecret = []byte(known_key)
184 permission_ttl = time.Duration(300) * time.Second
186 // When a permission key is available, the locator returned
187 // from an authenticated PUT request will be signed.
189 // Authenticated PUT, signed locator
190 // => OK (signed response)
191 response = IssueRequest(rest,
194 uri: unsigned_locator,
195 request_body: TEST_BLOCK,
196 api_token: known_token,
200 "Authenticated PUT, signed locator, with server key",
201 http.StatusOK, response)
202 response_locator := strings.TrimSpace(response.Body.String())
203 if !VerifySignature(response_locator, known_token) {
204 t.Errorf("Authenticated PUT, signed locator, with server key:\n"+
205 "response '%s' does not contain a valid signature",
209 // Unauthenticated PUT, unsigned locator
211 response = IssueRequest(rest,
214 uri: unsigned_locator,
215 request_body: TEST_BLOCK,
219 "Unauthenticated PUT, unsigned locator, with server key",
220 http.StatusOK, response)
222 "Unauthenticated PUT, unsigned locator, with server key",
223 TEST_HASH_PUT_RESPONSE, response)
226 // Test /index requests:
227 // - enforce_permissions off | unauthenticated /index request
228 // - enforce_permissions off | unauthenticated /index/prefix request
229 // - enforce_permissions off | authenticated /index request | non-superuser
230 // - enforce_permissions off | authenticated /index/prefix request | non-superuser
231 // - enforce_permissions off | authenticated /index request | superuser
232 // - enforce_permissions off | authenticated /index/prefix request | superuser
233 // - enforce_permissions on | unauthenticated /index request
234 // - enforce_permissions on | unauthenticated /index/prefix request
235 // - enforce_permissions on | authenticated /index request | non-superuser
236 // - enforce_permissions on | authenticated /index/prefix request | non-superuser
237 // - enforce_permissions on | authenticated /index request | superuser
238 // - enforce_permissions on | authenticated /index/prefix request | superuser
240 // The only /index requests that should succeed are those issued by the
241 // superuser when enforce_permissions = true.
243 func TestIndexHandler(t *testing.T) {
246 // Set up Keep volumes and populate them.
247 // Include multiple blocks on different volumes, and
248 // some metadata files (which should be omitted from index listings)
249 KeepVM = MakeTestVolumeManager(2)
252 vols := KeepVM.Volumes()
253 vols[0].Put(TEST_HASH, TEST_BLOCK)
254 vols[1].Put(TEST_HASH_2, TEST_BLOCK_2)
255 vols[0].Put(TEST_HASH+".meta", []byte("metadata"))
256 vols[1].Put(TEST_HASH_2+".meta", []byte("metadata"))
258 // Set up a REST router for testing the handlers.
259 rest := MakeRESTRouter()
261 data_manager_token = "DATA MANAGER TOKEN"
263 unauthenticated_req := &RequestTester{
267 authenticated_req := &RequestTester{
270 api_token: known_token,
272 superuser_req := &RequestTester{
275 api_token: data_manager_token,
277 unauth_prefix_req := &RequestTester{
279 uri: "/index/" + TEST_HASH[0:3],
281 auth_prefix_req := &RequestTester{
283 uri: "/index/" + TEST_HASH[0:3],
284 api_token: known_token,
286 superuser_prefix_req := &RequestTester{
288 uri: "/index/" + TEST_HASH[0:3],
289 api_token: data_manager_token,
292 // ----------------------------
293 // enforce_permissions disabled
294 // All /index requests should fail.
295 enforce_permissions = false
297 // unauthenticated /index request
298 // => PermissionError
299 response := IssueRequest(rest, unauthenticated_req)
301 "enforce_permissions off, unauthenticated request",
302 PermissionError.HTTPCode,
305 // unauthenticated /index/prefix request
306 // => PermissionError
307 response = IssueRequest(rest, unauth_prefix_req)
309 "enforce_permissions off, unauthenticated /index/prefix request",
310 PermissionError.HTTPCode,
313 // authenticated /index request, non-superuser
314 // => PermissionError
315 response = IssueRequest(rest, authenticated_req)
317 "enforce_permissions off, authenticated request, non-superuser",
318 PermissionError.HTTPCode,
321 // authenticated /index/prefix request, non-superuser
322 // => PermissionError
323 response = IssueRequest(rest, auth_prefix_req)
325 "enforce_permissions off, authenticated /index/prefix request, non-superuser",
326 PermissionError.HTTPCode,
329 // authenticated /index request, superuser
330 // => PermissionError
331 response = IssueRequest(rest, superuser_req)
333 "enforce_permissions off, superuser request",
334 PermissionError.HTTPCode,
337 // superuser /index/prefix request
338 // => PermissionError
339 response = IssueRequest(rest, superuser_prefix_req)
341 "enforce_permissions off, superuser /index/prefix request",
342 PermissionError.HTTPCode,
345 // ---------------------------
346 // enforce_permissions enabled
347 // Only the superuser should be allowed to issue /index requests.
348 enforce_permissions = true
350 // unauthenticated /index request
351 // => PermissionError
352 response = IssueRequest(rest, unauthenticated_req)
354 "enforce_permissions on, unauthenticated request",
355 PermissionError.HTTPCode,
358 // unauthenticated /index/prefix request
359 // => PermissionError
360 response = IssueRequest(rest, unauth_prefix_req)
362 "permissions on, unauthenticated /index/prefix request",
363 PermissionError.HTTPCode,
366 // authenticated /index request, non-superuser
367 // => PermissionError
368 response = IssueRequest(rest, authenticated_req)
370 "permissions on, authenticated request, non-superuser",
371 PermissionError.HTTPCode,
374 // authenticated /index/prefix request, non-superuser
375 // => PermissionError
376 response = IssueRequest(rest, auth_prefix_req)
378 "permissions on, authenticated /index/prefix request, non-superuser",
379 PermissionError.HTTPCode,
382 // superuser /index request
384 response = IssueRequest(rest, superuser_req)
386 "permissions on, superuser request",
390 expected := `^` + TEST_HASH + `\+\d+ \d+\n` +
391 TEST_HASH_2 + `\+\d+ \d+\n$`
392 match, _ := regexp.MatchString(expected, response.Body.String())
395 "permissions on, superuser request: expected %s, got:\n%s",
396 expected, response.Body.String())
399 // superuser /index/prefix request
401 response = IssueRequest(rest, superuser_prefix_req)
403 "permissions on, superuser request",
407 expected = `^` + TEST_HASH + `\+\d+ \d+\n$`
408 match, _ = regexp.MatchString(expected, response.Body.String())
411 "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
412 expected, response.Body.String())
420 // With no token and with a non-data-manager token:
421 // * Delete existing block
422 // (test for 403 Forbidden, confirm block not deleted)
424 // With data manager token:
426 // * Delete existing block
427 // (test for 200 OK, response counts, confirm block deleted)
429 // * Delete nonexistent block
430 // (test for 200 OK, response counts)
434 // * Delete block on read-only and read-write volume
435 // (test for 200 OK, response with copies_deleted=1,
436 // copies_failed=1, confirm block deleted only on r/w volume)
438 // * Delete block on read-only volume only
439 // (test for 200 OK, response with copies_deleted=0, copies_failed=1,
440 // confirm block not deleted)
442 func TestDeleteHandler(t *testing.T) {
445 // Set up Keep volumes and populate them.
446 // Include multiple blocks on different volumes, and
447 // some metadata files (which should be omitted from index listings)
448 KeepVM = MakeTestVolumeManager(2)
451 vols := KeepVM.Volumes()
452 vols[0].Put(TEST_HASH, TEST_BLOCK)
454 // Explicitly set the permission_ttl to 0 for these
455 // tests, to ensure the MockVolume deletes the blocks
456 // even though they have just been created.
457 permission_ttl = time.Duration(0)
459 // Set up a REST router for testing the handlers.
460 rest := MakeRESTRouter()
462 var user_token = "NOT DATA MANAGER TOKEN"
463 data_manager_token = "DATA MANAGER TOKEN"
465 unauth_req := &RequestTester{
467 uri: "/" + TEST_HASH,
470 user_req := &RequestTester{
472 uri: "/" + TEST_HASH,
473 api_token: user_token,
476 superuser_existing_block_req := &RequestTester{
478 uri: "/" + TEST_HASH,
479 api_token: data_manager_token,
482 superuser_nonexistent_block_req := &RequestTester{
484 uri: "/" + TEST_HASH_2,
485 api_token: data_manager_token,
488 // Unauthenticated request returns PermissionError.
489 var response *httptest.ResponseRecorder
490 response = IssueRequest(rest, unauth_req)
492 "unauthenticated request",
493 PermissionError.HTTPCode,
496 // Authenticated non-admin request returns PermissionError.
497 response = IssueRequest(rest, user_req)
499 "authenticated non-admin request",
500 PermissionError.HTTPCode,
503 // Authenticated admin request for nonexistent block.
504 type deletecounter struct {
505 Deleted int `json:"copies_deleted"`
506 Failed int `json:"copies_failed"`
508 var response_dc, expected_dc deletecounter
510 response = IssueRequest(rest, superuser_nonexistent_block_req)
512 "data manager request, nonexistent block",
516 // Authenticated admin request for existing block while never_delete is set.
518 response = IssueRequest(rest, superuser_existing_block_req)
520 "authenticated request, existing block, method disabled",
521 MethodDisabledError.HTTPCode,
525 // Authenticated admin request for existing block.
526 response = IssueRequest(rest, superuser_existing_block_req)
528 "data manager request, existing block",
531 // Expect response {"copies_deleted":1,"copies_failed":0}
532 expected_dc = deletecounter{1, 0}
533 json.NewDecoder(response.Body).Decode(&response_dc)
534 if response_dc != expected_dc {
535 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
536 expected_dc, response_dc)
538 // Confirm the block has been deleted
539 _, err := vols[0].Get(TEST_HASH)
540 var block_deleted = os.IsNotExist(err)
542 t.Error("superuser_existing_block_req: block not deleted")
545 // A DELETE request on a block newer than permission_ttl should return
546 // success but leave the block on the volume.
547 vols[0].Put(TEST_HASH, TEST_BLOCK)
548 permission_ttl = time.Duration(1) * time.Hour
550 response = IssueRequest(rest, superuser_existing_block_req)
552 "data manager request, existing block",
555 // Expect response {"copies_deleted":1,"copies_failed":0}
556 expected_dc = deletecounter{1, 0}
557 json.NewDecoder(response.Body).Decode(&response_dc)
558 if response_dc != expected_dc {
559 t.Errorf("superuser_existing_block_req\nexpected: %+v\nreceived: %+v",
560 expected_dc, response_dc)
562 // Confirm the block has NOT been deleted.
563 _, err = vols[0].Get(TEST_HASH)
565 t.Errorf("testing delete on new block: %s\n", err)
571 // Test handling of the PUT /pull statement.
573 // Cases tested: syntactically valid and invalid pull lists, from the
574 // data manager and from unprivileged users:
576 // 1. Valid pull list from an ordinary user
577 // (expected result: 401 Unauthorized)
579 // 2. Invalid pull request from an ordinary user
580 // (expected result: 401 Unauthorized)
582 // 3. Valid pull request from the data manager
583 // (expected result: 200 OK with request body "Received 3 pull
586 // 4. Invalid pull request from the data manager
587 // (expected result: 400 Bad Request)
589 // Test that in the end, the pull manager received a good pull list with
590 // the expected number of requests.
592 // TODO(twp): test concurrency: launch 100 goroutines to update the
593 // pull list simultaneously. Make sure that none of them return 400
594 // Bad Request and that pullmgr.GetList() returns a valid list.
596 func TestPullHandler(t *testing.T) {
599 // Set up a REST router for testing the handlers.
600 rest := MakeRESTRouter()
602 var user_token = "USER TOKEN"
603 data_manager_token = "DATA MANAGER TOKEN"
605 good_json := []byte(`[
607 "locator":"locator_with_two_servers",
614 "locator":"locator_with_no_servers",
619 "servers":["empty_locator"]
623 bad_json := []byte(`{ "key":"I'm a little teapot" }`)
625 type pullTest struct {
631 var testcases = []pullTest{
633 "user token, good request",
634 RequestTester{"/pull", user_token, "PUT", good_json},
635 http.StatusUnauthorized,
639 "user token, bad request",
640 RequestTester{"/pull", user_token, "PUT", bad_json},
641 http.StatusUnauthorized,
645 "data manager token, good request",
646 RequestTester{"/pull", data_manager_token, "PUT", good_json},
648 "Received 3 pull requests\n",
651 "data manager token, bad request",
652 RequestTester{"/pull", data_manager_token, "PUT", bad_json},
653 http.StatusBadRequest,
658 for _, tst := range testcases {
659 response := IssueRequest(rest, &tst.req)
660 ExpectStatusCode(t, tst.name, tst.response_code, response)
661 ExpectBody(t, tst.name, tst.response_body, response)
664 // The Keep pull manager should have received one good list with 3
666 var saved_pull_list = pullmgr.GetList()
667 if len(saved_pull_list) != 3 {
669 "saved_pull_list: expected 3 elements, got %d\nsaved_pull_list = %v",
670 len(saved_pull_list), saved_pull_list)
674 // ====================
676 // ====================
678 // IssueTestRequest executes an HTTP request described by rt, to a
679 // specified REST router. It returns the HTTP response to the request.
680 func IssueRequest(router *mux.Router, rt *RequestTester) *httptest.ResponseRecorder {
681 response := httptest.NewRecorder()
682 body := bytes.NewReader(rt.request_body)
683 req, _ := http.NewRequest(rt.method, rt.uri, body)
684 if rt.api_token != "" {
685 req.Header.Set("Authorization", "OAuth2 "+rt.api_token)
687 router.ServeHTTP(response, req)
691 // ExpectStatusCode checks whether a response has the specified status code,
692 // and reports a test failure if not.
693 func ExpectStatusCode(
697 response *httptest.ResponseRecorder) {
698 if response.Code != expected_status {
699 t.Errorf("%s: expected status %s, got %+v",
700 testname, expected_status, response)
707 expected_body string,
708 response *httptest.ResponseRecorder) {
709 if response.Body.String() != expected_body {
710 t.Errorf("%s: expected response body '%s', got %+v",
711 testname, expected_body, response)