Merge branch 'master' into 3339-truncate-project-descriptions
[arvados.git] / services / keepstore / handler_test.go
1 // Tests for Keep HTTP handlers:
2 //
3 //     GetBlockHandler
4 //     PutBlockHandler
5 //     IndexHandler
6 //
7 // The HTTP handlers are responsible for enforcing permission policy,
8 // so these tests must exercise all possible permission permutations.
9
10 package main
11
12 import (
13         "bytes"
14         "encoding/json"
15         "fmt"
16         "github.com/gorilla/mux"
17         "net/http"
18         "net/http/httptest"
19         "os"
20         "regexp"
21         "strings"
22         "testing"
23         "time"
24 )
25
26 // A RequestTester represents the parameters for an HTTP request to
27 // be issued on behalf of a unit test.
28 type RequestTester struct {
29         uri          string
30         api_token    string
31         method       string
32         request_body []byte
33 }
34
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
41 //
42 func TestGetHandler(t *testing.T) {
43         defer teardown()
44
45         // Prepare two test Keep volumes. Our block is stored on the second volume.
46         KeepVM = MakeTestVolumeManager(2)
47         defer KeepVM.Quit()
48
49         vols := KeepVM.Volumes()
50         if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil {
51                 t.Error(err)
52         }
53
54         // Set up a REST router for testing the handlers.
55         rest := MakeRESTRouter()
56
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
62
63         var (
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)
69         )
70
71         // -----------------
72         // Test unauthenticated request with permissions off.
73         enforce_permissions = false
74
75         // Unauthenticated request, unsigned locator
76         // => OK
77         response := IssueRequest(rest,
78                 &RequestTester{
79                         method: "GET",
80                         uri:    unsigned_locator,
81                 })
82         ExpectStatusCode(t,
83                 "Unauthenticated request, unsigned locator", http.StatusOK, response)
84         ExpectBody(t,
85                 "Unauthenticated request, unsigned locator",
86                 string(TEST_BLOCK),
87                 response)
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)
92         }
93
94         // ----------------
95         // Permissions: on.
96         enforce_permissions = true
97
98         // Authenticated request, signed locator
99         // => OK
100         response = IssueRequest(rest, &RequestTester{
101                 method:    "GET",
102                 uri:       signed_locator,
103                 api_token: known_token,
104         })
105         ExpectStatusCode(t,
106                 "Authenticated request, signed locator", http.StatusOK, response)
107         ExpectBody(t,
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)
113         }
114
115         // Authenticated request, unsigned locator
116         // => PermissionError
117         response = IssueRequest(rest, &RequestTester{
118                 method:    "GET",
119                 uri:       unsigned_locator,
120                 api_token: known_token,
121         })
122         ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
123
124         // Unauthenticated request, signed locator
125         // => PermissionError
126         response = IssueRequest(rest, &RequestTester{
127                 method: "GET",
128                 uri:    signed_locator,
129         })
130         ExpectStatusCode(t,
131                 "Unauthenticated request, signed locator",
132                 PermissionError.HTTPCode, response)
133
134         // Authenticated request, expired locator
135         // => ExpiredError
136         response = IssueRequest(rest, &RequestTester{
137                 method:    "GET",
138                 uri:       expired_locator,
139                 api_token: known_token,
140         })
141         ExpectStatusCode(t,
142                 "Authenticated request, expired locator",
143                 ExpiredError.HTTPCode, response)
144 }
145
146 // Test PutBlockHandler on the following situations:
147 //   - no server key
148 //   - with server key, authenticated request, unsigned locator
149 //   - with server key, unauthenticated request, unsigned locator
150 //
151 func TestPutHandler(t *testing.T) {
152         defer teardown()
153
154         // Prepare two test Keep volumes.
155         KeepVM = MakeTestVolumeManager(2)
156         defer KeepVM.Quit()
157
158         // Set up a REST router for testing the handlers.
159         rest := MakeRESTRouter()
160
161         // --------------
162         // No server key.
163
164         // Unauthenticated request, no server key
165         // => OK (unsigned response)
166         unsigned_locator := "/" + TEST_HASH
167         response := IssueRequest(rest,
168                 &RequestTester{
169                         method:       "PUT",
170                         uri:          unsigned_locator,
171                         request_body: TEST_BLOCK,
172                 })
173
174         ExpectStatusCode(t,
175                 "Unauthenticated request, no server key", http.StatusOK, response)
176         ExpectBody(t,
177                 "Unauthenticated request, no server key",
178                 TEST_HASH_PUT_RESPONSE, response)
179
180         // ------------------
181         // With a server key.
182
183         PermissionSecret = []byte(known_key)
184         permission_ttl = time.Duration(300) * time.Second
185
186         // When a permission key is available, the locator returned
187         // from an authenticated PUT request will be signed.
188
189         // Authenticated PUT, signed locator
190         // => OK (signed response)
191         response = IssueRequest(rest,
192                 &RequestTester{
193                         method:       "PUT",
194                         uri:          unsigned_locator,
195                         request_body: TEST_BLOCK,
196                         api_token:    known_token,
197                 })
198
199         ExpectStatusCode(t,
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",
206                         response_locator)
207         }
208
209         // Unauthenticated PUT, unsigned locator
210         // => OK
211         response = IssueRequest(rest,
212                 &RequestTester{
213                         method:       "PUT",
214                         uri:          unsigned_locator,
215                         request_body: TEST_BLOCK,
216                 })
217
218         ExpectStatusCode(t,
219                 "Unauthenticated PUT, unsigned locator, with server key",
220                 http.StatusOK, response)
221         ExpectBody(t,
222                 "Unauthenticated PUT, unsigned locator, with server key",
223                 TEST_HASH_PUT_RESPONSE, response)
224 }
225
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
239 //
240 // The only /index requests that should succeed are those issued by the
241 // superuser when enforce_permissions = true.
242 //
243 func TestIndexHandler(t *testing.T) {
244         defer teardown()
245
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)
250         defer KeepVM.Quit()
251
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"))
257
258         // Set up a REST router for testing the handlers.
259         rest := MakeRESTRouter()
260
261         data_manager_token = "DATA MANAGER TOKEN"
262
263         unauthenticated_req := &RequestTester{
264                 method: "GET",
265                 uri:    "/index",
266         }
267         authenticated_req := &RequestTester{
268                 method:    "GET",
269                 uri:       "/index",
270                 api_token: known_token,
271         }
272         superuser_req := &RequestTester{
273                 method:    "GET",
274                 uri:       "/index",
275                 api_token: data_manager_token,
276         }
277         unauth_prefix_req := &RequestTester{
278                 method: "GET",
279                 uri:    "/index/" + TEST_HASH[0:3],
280         }
281         auth_prefix_req := &RequestTester{
282                 method:    "GET",
283                 uri:       "/index/" + TEST_HASH[0:3],
284                 api_token: known_token,
285         }
286         superuser_prefix_req := &RequestTester{
287                 method:    "GET",
288                 uri:       "/index/" + TEST_HASH[0:3],
289                 api_token: data_manager_token,
290         }
291
292         // ----------------------------
293         // enforce_permissions disabled
294         // All /index requests should fail.
295         enforce_permissions = false
296
297         // unauthenticated /index request
298         // => PermissionError
299         response := IssueRequest(rest, unauthenticated_req)
300         ExpectStatusCode(t,
301                 "enforce_permissions off, unauthenticated request",
302                 PermissionError.HTTPCode,
303                 response)
304
305         // unauthenticated /index/prefix request
306         // => PermissionError
307         response = IssueRequest(rest, unauth_prefix_req)
308         ExpectStatusCode(t,
309                 "enforce_permissions off, unauthenticated /index/prefix request",
310                 PermissionError.HTTPCode,
311                 response)
312
313         // authenticated /index request, non-superuser
314         // => PermissionError
315         response = IssueRequest(rest, authenticated_req)
316         ExpectStatusCode(t,
317                 "enforce_permissions off, authenticated request, non-superuser",
318                 PermissionError.HTTPCode,
319                 response)
320
321         // authenticated /index/prefix request, non-superuser
322         // => PermissionError
323         response = IssueRequest(rest, auth_prefix_req)
324         ExpectStatusCode(t,
325                 "enforce_permissions off, authenticated /index/prefix request, non-superuser",
326                 PermissionError.HTTPCode,
327                 response)
328
329         // authenticated /index request, superuser
330         // => PermissionError
331         response = IssueRequest(rest, superuser_req)
332         ExpectStatusCode(t,
333                 "enforce_permissions off, superuser request",
334                 PermissionError.HTTPCode,
335                 response)
336
337         // superuser /index/prefix request
338         // => PermissionError
339         response = IssueRequest(rest, superuser_prefix_req)
340         ExpectStatusCode(t,
341                 "enforce_permissions off, superuser /index/prefix request",
342                 PermissionError.HTTPCode,
343                 response)
344
345         // ---------------------------
346         // enforce_permissions enabled
347         // Only the superuser should be allowed to issue /index requests.
348         enforce_permissions = true
349
350         // unauthenticated /index request
351         // => PermissionError
352         response = IssueRequest(rest, unauthenticated_req)
353         ExpectStatusCode(t,
354                 "enforce_permissions on, unauthenticated request",
355                 PermissionError.HTTPCode,
356                 response)
357
358         // unauthenticated /index/prefix request
359         // => PermissionError
360         response = IssueRequest(rest, unauth_prefix_req)
361         ExpectStatusCode(t,
362                 "permissions on, unauthenticated /index/prefix request",
363                 PermissionError.HTTPCode,
364                 response)
365
366         // authenticated /index request, non-superuser
367         // => PermissionError
368         response = IssueRequest(rest, authenticated_req)
369         ExpectStatusCode(t,
370                 "permissions on, authenticated request, non-superuser",
371                 PermissionError.HTTPCode,
372                 response)
373
374         // authenticated /index/prefix request, non-superuser
375         // => PermissionError
376         response = IssueRequest(rest, auth_prefix_req)
377         ExpectStatusCode(t,
378                 "permissions on, authenticated /index/prefix request, non-superuser",
379                 PermissionError.HTTPCode,
380                 response)
381
382         // superuser /index request
383         // => OK
384         response = IssueRequest(rest, superuser_req)
385         ExpectStatusCode(t,
386                 "permissions on, superuser request",
387                 http.StatusOK,
388                 response)
389
390         expected := `^` + TEST_HASH + `\+\d+ \d+\n` +
391                 TEST_HASH_2 + `\+\d+ \d+\n$`
392         match, _ := regexp.MatchString(expected, response.Body.String())
393         if !match {
394                 t.Errorf(
395                         "permissions on, superuser request: expected %s, got:\n%s",
396                         expected, response.Body.String())
397         }
398
399         // superuser /index/prefix request
400         // => OK
401         response = IssueRequest(rest, superuser_prefix_req)
402         ExpectStatusCode(t,
403                 "permissions on, superuser request",
404                 http.StatusOK,
405                 response)
406
407         expected = `^` + TEST_HASH + `\+\d+ \d+\n$`
408         match, _ = regexp.MatchString(expected, response.Body.String())
409         if !match {
410                 t.Errorf(
411                         "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
412                         expected, response.Body.String())
413         }
414 }
415
416 // TestDeleteHandler
417 //
418 // Cases tested:
419 //
420 //   With no token and with a non-data-manager token:
421 //   * Delete existing block
422 //     (test for 403 Forbidden, confirm block not deleted)
423 //
424 //   With data manager token:
425 //
426 //   * Delete existing block
427 //     (test for 200 OK, response counts, confirm block deleted)
428 //
429 //   * Delete nonexistent block
430 //     (test for 200 OK, response counts)
431 //
432 //   TODO(twp):
433 //
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)
437 //
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)
441 //
442 func TestDeleteHandler(t *testing.T) {
443         defer teardown()
444
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)
449         defer KeepVM.Quit()
450
451         vols := KeepVM.Volumes()
452         vols[0].Put(TEST_HASH, TEST_BLOCK)
453
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)
458
459         // Set up a REST router for testing the handlers.
460         rest := MakeRESTRouter()
461
462         var user_token = "NOT DATA MANAGER TOKEN"
463         data_manager_token = "DATA MANAGER TOKEN"
464
465         unauth_req := &RequestTester{
466                 method: "DELETE",
467                 uri:    "/" + TEST_HASH,
468         }
469
470         user_req := &RequestTester{
471                 method:    "DELETE",
472                 uri:       "/" + TEST_HASH,
473                 api_token: user_token,
474         }
475
476         superuser_existing_block_req := &RequestTester{
477                 method:    "DELETE",
478                 uri:       "/" + TEST_HASH,
479                 api_token: data_manager_token,
480         }
481
482         superuser_nonexistent_block_req := &RequestTester{
483                 method:    "DELETE",
484                 uri:       "/" + TEST_HASH_2,
485                 api_token: data_manager_token,
486         }
487
488         // Unauthenticated request returns PermissionError.
489         var response *httptest.ResponseRecorder
490         response = IssueRequest(rest, unauth_req)
491         ExpectStatusCode(t,
492                 "unauthenticated request",
493                 PermissionError.HTTPCode,
494                 response)
495
496         // Authenticated non-admin request returns PermissionError.
497         response = IssueRequest(rest, user_req)
498         ExpectStatusCode(t,
499                 "authenticated non-admin request",
500                 PermissionError.HTTPCode,
501                 response)
502
503         // Authenticated admin request for nonexistent block.
504         type deletecounter struct {
505                 Deleted int `json:"copies_deleted"`
506                 Failed  int `json:"copies_failed"`
507         }
508         var response_dc, expected_dc deletecounter
509
510         response = IssueRequest(rest, superuser_nonexistent_block_req)
511         ExpectStatusCode(t,
512                 "data manager request, nonexistent block",
513                 http.StatusNotFound,
514                 response)
515
516         // Authenticated admin request for existing block while never_delete is set.
517         never_delete = true
518         response = IssueRequest(rest, superuser_existing_block_req)
519         ExpectStatusCode(t,
520                 "authenticated request, existing block, method disabled",
521                 MethodDisabledError.HTTPCode,
522                 response)
523         never_delete = false
524
525         // Authenticated admin request for existing block.
526         response = IssueRequest(rest, superuser_existing_block_req)
527         ExpectStatusCode(t,
528                 "data manager request, existing block",
529                 http.StatusOK,
530                 response)
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)
537         }
538         // Confirm the block has been deleted
539         _, err := vols[0].Get(TEST_HASH)
540         var block_deleted = os.IsNotExist(err)
541         if !block_deleted {
542                 t.Error("superuser_existing_block_req: block not deleted")
543         }
544
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
549
550         response = IssueRequest(rest, superuser_existing_block_req)
551         ExpectStatusCode(t,
552                 "data manager request, existing block",
553                 http.StatusOK,
554                 response)
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)
561         }
562         // Confirm the block has NOT been deleted.
563         _, err = vols[0].Get(TEST_HASH)
564         if err != nil {
565                 t.Errorf("testing delete on new block: %s\n", err)
566         }
567 }
568
569 // TestPullHandler
570 //
571 // Test handling of the PUT /pull statement.
572 //
573 // Cases tested: syntactically valid and invalid pull lists, from the
574 // data manager and from unprivileged users:
575 //
576 //   1. Valid pull list from an ordinary user
577 //      (expected result: 401 Unauthorized)
578 //
579 //   2. Invalid pull request from an ordinary user
580 //      (expected result: 401 Unauthorized)
581 //
582 //   3. Valid pull request from the data manager
583 //      (expected result: 200 OK with request body "Received 3 pull
584 //      requests"
585 //
586 //   4. Invalid pull request from the data manager
587 //      (expected result: 400 Bad Request)
588 //
589 // Test that in the end, the pull manager received a good pull list with
590 // the expected number of requests.
591 //
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.
595 //
596 func TestPullHandler(t *testing.T) {
597         defer teardown()
598
599         // Set up a REST router for testing the handlers.
600         rest := MakeRESTRouter()
601
602         var user_token = "USER TOKEN"
603         data_manager_token = "DATA MANAGER TOKEN"
604
605         good_json := []byte(`[
606                 {
607                         "locator":"locator_with_two_servers",
608                         "servers":[
609                                 "server1",
610                                 "server2"
611                         ]
612                 },
613                 {
614                         "locator":"locator_with_no_servers",
615                         "servers":[]
616                 },
617                 {
618                         "locator":"",
619                         "servers":["empty_locator"]
620                 }
621         ]`)
622
623         bad_json := []byte(`{ "key":"I'm a little teapot" }`)
624
625         type pullTest struct {
626                 name          string
627                 req           RequestTester
628                 response_code int
629                 response_body string
630         }
631         var testcases = []pullTest{
632                 {
633                         "user token, good request",
634                         RequestTester{"/pull", user_token, "PUT", good_json},
635                         http.StatusUnauthorized,
636                         "Unauthorized\n",
637                 },
638                 {
639                         "user token, bad request",
640                         RequestTester{"/pull", user_token, "PUT", bad_json},
641                         http.StatusUnauthorized,
642                         "Unauthorized\n",
643                 },
644                 {
645                         "data manager token, good request",
646                         RequestTester{"/pull", data_manager_token, "PUT", good_json},
647                         http.StatusOK,
648                         "Received 3 pull requests\n",
649                 },
650                 {
651                         "data manager token, bad request",
652                         RequestTester{"/pull", data_manager_token, "PUT", bad_json},
653                         http.StatusBadRequest,
654                         "Bad Request\n",
655                 },
656         }
657
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)
662         }
663
664         // The Keep pull manager should have received one good list with 3
665         // requests on it.
666         var saved_pull_list = pullmgr.GetList()
667         if len(saved_pull_list) != 3 {
668                 t.Errorf(
669                         "saved_pull_list: expected 3 elements, got %d\nsaved_pull_list = %v",
670                         len(saved_pull_list), saved_pull_list)
671         }
672 }
673
674 // ====================
675 // Helper functions
676 // ====================
677
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)
686         }
687         router.ServeHTTP(response, req)
688         return response
689 }
690
691 // ExpectStatusCode checks whether a response has the specified status code,
692 // and reports a test failure if not.
693 func ExpectStatusCode(
694         t *testing.T,
695         testname string,
696         expected_status int,
697         response *httptest.ResponseRecorder) {
698         if response.Code != expected_status {
699                 t.Errorf("%s: expected status %s, got %+v",
700                         testname, expected_status, response)
701         }
702 }
703
704 func ExpectBody(
705         t *testing.T,
706         testname string,
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)
712         }
713 }