cab203d5a5904c5d40cb32486227061c24d31e82
[arvados.git] / services / keep / src / keep / 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 func() { 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  = "http://localhost:25107/" + TEST_HASH
65                 valid_timestamp   = time.Now().Add(permission_ttl)
66                 expired_timestamp = time.Now().Add(-time.Hour)
67                 signed_locator    = "http://localhost:25107/" + SignLocator(TEST_HASH, known_token, valid_timestamp)
68                 expired_locator   = "http://localhost:25107/" + 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 func() { 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 := "http://localhost:25107/" + 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 func() { 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:    "http://localhost:25107/index",
266         }
267         authenticated_req := &RequestTester{
268                 method:    "GET",
269                 uri:       "http://localhost:25107/index",
270                 api_token: known_token,
271         }
272         superuser_req := &RequestTester{
273                 method:    "GET",
274                 uri:       "http://localhost:25107/index",
275                 api_token: data_manager_token,
276         }
277         unauth_prefix_req := &RequestTester{
278                 method: "GET",
279                 uri:    "http://localhost:25107/index/" + TEST_HASH[0:3],
280         }
281         auth_prefix_req := &RequestTester{
282                 method:    "GET",
283                 uri:       "http://localhost:25107/index/" + TEST_HASH[0:3],
284                 api_token: known_token,
285         }
286         superuser_prefix_req := &RequestTester{
287                 method:    "GET",
288                 uri:       "http://localhost:25107/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 to test:
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 volume
435 //     (test for 200 OK, response counts, confirm block not deleted)
436 //
437 func TestDeleteHandler(t *testing.T) {
438         defer teardown()
439
440         // Set up Keep volumes and populate them.
441         // Include multiple blocks on different volumes, and
442         // some metadata files (which should be omitted from index listings)
443         KeepVM = MakeTestVolumeManager(2)
444         defer func() { KeepVM.Quit() }()
445
446         vols := KeepVM.Volumes()
447         vols[0].Put(TEST_HASH, TEST_BLOCK)
448
449         // Set up a REST router for testing the handlers.
450         rest := MakeRESTRouter()
451
452         var user_token = "NOT DATA MANAGER TOKEN"
453         data_manager_token = "DATA MANAGER TOKEN"
454
455         unauth_req := &RequestTester{
456                 method: "DELETE",
457                 uri:    "http://localhost:25107/" + TEST_HASH,
458         }
459
460         user_req := &RequestTester{
461                 method:    "DELETE",
462                 uri:       "http://localhost:25107/" + TEST_HASH,
463                 api_token: user_token,
464         }
465
466         superuser_existing_block_req := &RequestTester{
467                 method:    "DELETE",
468                 uri:       "http://localhost:25107/" + TEST_HASH,
469                 api_token: data_manager_token,
470         }
471
472         superuser_nonexistent_block_req := &RequestTester{
473                 method:    "DELETE",
474                 uri:       "http://localhost:25107/" + TEST_HASH_2,
475                 api_token: data_manager_token,
476         }
477
478         // Unauthenticated request returns PermissionError.
479         var response *httptest.ResponseRecorder
480         response = IssueRequest(rest, unauth_req)
481         ExpectStatusCode(t,
482                 "unauthenticated request",
483                 PermissionError.HTTPCode,
484                 response)
485
486         // Authenticated non-admin request returns PermissionError.
487         response = IssueRequest(rest, user_req)
488         ExpectStatusCode(t,
489                 "authenticated non-admin request",
490                 PermissionError.HTTPCode,
491                 response)
492
493         // Authenticated admin request for nonexistent block.
494         type deletecounter struct {
495                 Deleted int `json:"copies_deleted"`
496                 Failed  int `json:"copies_failed"`
497         }
498         var dc deletecounter
499
500         response = IssueRequest(rest, superuser_nonexistent_block_req)
501         ExpectStatusCode(t,
502                 "data manager request, nonexistent block",
503                 http.StatusOK,
504                 response)
505         // Expect response {"copies_deleted":0,"copies_failed":0}
506         json.NewDecoder(response.Body).Decode(&dc)
507         if dc.Deleted != 0 || dc.Failed != 0 {
508                 t.Errorf("superuser_nonexistent_block_req: response = %+v", dc)
509         }
510
511         // Authenticated admin request for existing block.
512         response = IssueRequest(rest, superuser_existing_block_req)
513         ExpectStatusCode(t,
514                 "data manager request, existing block",
515                 http.StatusOK,
516                 response)
517         // Expect response {"copies_deleted":1,"copies_failed":0}
518         json.NewDecoder(response.Body).Decode(&dc)
519         if dc.Deleted != 1 || dc.Failed != 0 {
520                 t.Errorf("superuser_existing_block_req: response = %+v", dc)
521         }
522         // Confirm the block has been deleted
523         _, err := vols[0].Get(TEST_HASH)
524         if !os.IsNotExist(err) {
525                 t.Error("superuser_existing_block_req: block not deleted")
526         }
527 }
528
529 // ====================
530 // Helper functions
531 // ====================
532
533 // IssueTestRequest executes an HTTP request described by rt, to a
534 // specified REST router.  It returns the HTTP response to the request.
535 func IssueRequest(router *mux.Router, rt *RequestTester) *httptest.ResponseRecorder {
536         response := httptest.NewRecorder()
537         body := bytes.NewReader(rt.request_body)
538         req, _ := http.NewRequest(rt.method, rt.uri, body)
539         if rt.api_token != "" {
540                 req.Header.Set("Authorization", "OAuth2 "+rt.api_token)
541         }
542         router.ServeHTTP(response, req)
543         return response
544 }
545
546 // ExpectStatusCode checks whether a response has the specified status code,
547 // and reports a test failure if not.
548 func ExpectStatusCode(
549         t *testing.T,
550         testname string,
551         expected_status int,
552         response *httptest.ResponseRecorder) {
553         if response.Code != expected_status {
554                 t.Errorf("%s: expected status %s, got %+v",
555                         testname, expected_status, response)
556         }
557 }
558
559 func ExpectBody(
560         t *testing.T,
561         testname string,
562         expected_body string,
563         response *httptest.ResponseRecorder) {
564         if response.Body.String() != expected_body {
565                 t.Errorf("%s: expected response body '%s', got %+v",
566                         testname, expected_body, response)
567         }
568 }