2800: Allow api() caller to specify api host and token.
[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         "fmt"
15         "github.com/gorilla/mux"
16         "net/http"
17         "net/http/httptest"
18         "regexp"
19         "strings"
20         "testing"
21         "time"
22 )
23
24 // A RequestTester represents the parameters for an HTTP request to
25 // be issued on behalf of a unit test.
26 type RequestTester struct {
27         uri          string
28         api_token    string
29         method       string
30         request_body []byte
31 }
32
33 // Test GetBlockHandler on the following situations:
34 //   - permissions off, unauthenticated request, unsigned locator
35 //   - permissions on, authenticated request, signed locator
36 //   - permissions on, authenticated request, unsigned locator
37 //   - permissions on, unauthenticated request, signed locator
38 //   - permissions on, authenticated request, expired locator
39 //
40 func TestGetHandler(t *testing.T) {
41         defer teardown()
42
43         // Prepare two test Keep volumes. Our block is stored on the second volume.
44         KeepVM = MakeTestVolumeManager(2)
45         defer func() { KeepVM.Quit() }()
46
47         vols := KeepVM.Volumes()
48         if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil {
49                 t.Error(err)
50         }
51
52         // Set up a REST router for testing the handlers.
53         rest := MakeRESTRouter()
54
55         // Create locators for testing.
56         // Turn on permission settings so we can generate signed locators.
57         enforce_permissions = true
58         PermissionSecret = []byte(known_key)
59         permission_ttl = time.Duration(300) * time.Second
60
61         var (
62                 unsigned_locator  = "http://localhost:25107/" + TEST_HASH
63                 valid_timestamp   = time.Now().Add(permission_ttl)
64                 expired_timestamp = time.Now().Add(-time.Hour)
65                 signed_locator    = "http://localhost:25107/" + SignLocator(TEST_HASH, known_token, valid_timestamp)
66                 expired_locator   = "http://localhost:25107/" + SignLocator(TEST_HASH, known_token, expired_timestamp)
67         )
68
69         // -----------------
70         // Test unauthenticated request with permissions off.
71         enforce_permissions = false
72
73         // Unauthenticated request, unsigned locator
74         // => OK
75         response := IssueRequest(rest,
76                 &RequestTester{
77                         method: "GET",
78                         uri:    unsigned_locator,
79                 })
80         ExpectStatusCode(t,
81                 "Unauthenticated request, unsigned locator", http.StatusOK, response)
82         ExpectBody(t,
83                 "Unauthenticated request, unsigned locator",
84                 string(TEST_BLOCK),
85                 response)
86         received_xbs := response.Header().Get("X-Block-Size")
87         expected_xbs := fmt.Sprintf("%d", len(TEST_BLOCK))
88         if received_xbs != expected_xbs {
89                 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
90         }
91
92         // ----------------
93         // Permissions: on.
94         enforce_permissions = true
95
96         // Authenticated request, signed locator
97         // => OK
98         response = IssueRequest(rest, &RequestTester{
99                 method:    "GET",
100                 uri:       signed_locator,
101                 api_token: known_token,
102         })
103         ExpectStatusCode(t,
104                 "Authenticated request, signed locator", http.StatusOK, response)
105         ExpectBody(t,
106                 "Authenticated request, signed locator", string(TEST_BLOCK), response)
107         received_xbs = response.Header().Get("X-Block-Size")
108         expected_xbs = fmt.Sprintf("%d", len(TEST_BLOCK))
109         if received_xbs != expected_xbs {
110                 t.Errorf("expected X-Block-Size %s, got %s", expected_xbs, received_xbs)
111         }
112
113         // Authenticated request, unsigned locator
114         // => PermissionError
115         response = IssueRequest(rest, &RequestTester{
116                 method:    "GET",
117                 uri:       unsigned_locator,
118                 api_token: known_token,
119         })
120         ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response)
121
122         // Unauthenticated request, signed locator
123         // => PermissionError
124         response = IssueRequest(rest, &RequestTester{
125                 method: "GET",
126                 uri:    signed_locator,
127         })
128         ExpectStatusCode(t,
129                 "Unauthenticated request, signed locator",
130                 PermissionError.HTTPCode, response)
131
132         // Authenticated request, expired locator
133         // => ExpiredError
134         response = IssueRequest(rest, &RequestTester{
135                 method:    "GET",
136                 uri:       expired_locator,
137                 api_token: known_token,
138         })
139         ExpectStatusCode(t,
140                 "Authenticated request, expired locator",
141                 ExpiredError.HTTPCode, response)
142 }
143
144 // Test PutBlockHandler on the following situations:
145 //   - no server key
146 //   - with server key, authenticated request, unsigned locator
147 //   - with server key, unauthenticated request, unsigned locator
148 //
149 func TestPutHandler(t *testing.T) {
150         defer teardown()
151
152         // Prepare two test Keep volumes.
153         KeepVM = MakeTestVolumeManager(2)
154         defer func() { KeepVM.Quit() }()
155
156         // Set up a REST router for testing the handlers.
157         rest := MakeRESTRouter()
158
159         // --------------
160         // No server key.
161
162         // Unauthenticated request, no server key
163         // => OK (unsigned response)
164         unsigned_locator := "http://localhost:25107/" + TEST_HASH
165         response := IssueRequest(rest,
166                 &RequestTester{
167                         method:       "PUT",
168                         uri:          unsigned_locator,
169                         request_body: TEST_BLOCK,
170                 })
171
172         ExpectStatusCode(t,
173                 "Unauthenticated request, no server key", http.StatusOK, response)
174         ExpectBody(t,
175                 "Unauthenticated request, no server key",
176                 TEST_HASH_PUT_RESPONSE, response)
177
178         // ------------------
179         // With a server key.
180
181         PermissionSecret = []byte(known_key)
182         permission_ttl = time.Duration(300) * time.Second
183
184         // When a permission key is available, the locator returned
185         // from an authenticated PUT request will be signed.
186
187         // Authenticated PUT, signed locator
188         // => OK (signed response)
189         response = IssueRequest(rest,
190                 &RequestTester{
191                         method:       "PUT",
192                         uri:          unsigned_locator,
193                         request_body: TEST_BLOCK,
194                         api_token:    known_token,
195                 })
196
197         ExpectStatusCode(t,
198                 "Authenticated PUT, signed locator, with server key",
199                 http.StatusOK, response)
200         response_locator := strings.TrimSpace(response.Body.String())
201         if !VerifySignature(response_locator, known_token) {
202                 t.Errorf("Authenticated PUT, signed locator, with server key:\n"+
203                         "response '%s' does not contain a valid signature",
204                         response_locator)
205         }
206
207         // Unauthenticated PUT, unsigned locator
208         // => OK
209         response = IssueRequest(rest,
210                 &RequestTester{
211                         method:       "PUT",
212                         uri:          unsigned_locator,
213                         request_body: TEST_BLOCK,
214                 })
215
216         ExpectStatusCode(t,
217                 "Unauthenticated PUT, unsigned locator, with server key",
218                 http.StatusOK, response)
219         ExpectBody(t,
220                 "Unauthenticated PUT, unsigned locator, with server key",
221                 TEST_HASH_PUT_RESPONSE, response)
222 }
223
224 // Test /index requests:
225 //   - enforce_permissions off | unauthenticated /index request
226 //   - enforce_permissions off | unauthenticated /index/prefix request
227 //   - enforce_permissions off | authenticated /index request        | non-superuser
228 //   - enforce_permissions off | authenticated /index/prefix request | non-superuser
229 //   - enforce_permissions off | authenticated /index request        | superuser
230 //   - enforce_permissions off | authenticated /index/prefix request | superuser
231 //   - enforce_permissions on  | unauthenticated /index request
232 //   - enforce_permissions on  | unauthenticated /index/prefix request
233 //   - enforce_permissions on  | authenticated /index request        | non-superuser
234 //   - enforce_permissions on  | authenticated /index/prefix request | non-superuser
235 //   - enforce_permissions on  | authenticated /index request        | superuser
236 //   - enforce_permissions on  | authenticated /index/prefix request | superuser
237 //
238 // The only /index requests that should succeed are those issued by the
239 // superuser when enforce_permissions = true.
240 //
241 func TestIndexHandler(t *testing.T) {
242         defer teardown()
243
244         // Set up Keep volumes and populate them.
245         // Include multiple blocks on different volumes, and
246         // some metadata files (which should be omitted from index listings)
247         KeepVM = MakeTestVolumeManager(2)
248         defer func() { KeepVM.Quit() }()
249
250         vols := KeepVM.Volumes()
251         vols[0].Put(TEST_HASH, TEST_BLOCK)
252         vols[1].Put(TEST_HASH_2, TEST_BLOCK_2)
253         vols[0].Put(TEST_HASH+".meta", []byte("metadata"))
254         vols[1].Put(TEST_HASH_2+".meta", []byte("metadata"))
255
256         // Set up a REST router for testing the handlers.
257         rest := MakeRESTRouter()
258
259         data_manager_token = "DATA MANAGER TOKEN"
260
261         unauthenticated_req := &RequestTester{
262                 method: "GET",
263                 uri:    "http://localhost:25107/index",
264         }
265         authenticated_req := &RequestTester{
266                 method:    "GET",
267                 uri:       "http://localhost:25107/index",
268                 api_token: known_token,
269         }
270         superuser_req := &RequestTester{
271                 method:    "GET",
272                 uri:       "http://localhost:25107/index",
273                 api_token: data_manager_token,
274         }
275         unauth_prefix_req := &RequestTester{
276                 method: "GET",
277                 uri:    "http://localhost:25107/index/" + TEST_HASH[0:3],
278         }
279         auth_prefix_req := &RequestTester{
280                 method:    "GET",
281                 uri:       "http://localhost:25107/index/" + TEST_HASH[0:3],
282                 api_token: known_token,
283         }
284         superuser_prefix_req := &RequestTester{
285                 method:    "GET",
286                 uri:       "http://localhost:25107/index/" + TEST_HASH[0:3],
287                 api_token: data_manager_token,
288         }
289
290         // ----------------------------
291         // enforce_permissions disabled
292         // All /index requests should fail.
293         enforce_permissions = false
294
295         // unauthenticated /index request
296         // => PermissionError
297         response := IssueRequest(rest, unauthenticated_req)
298         ExpectStatusCode(t,
299                 "enforce_permissions off, unauthenticated request",
300                 PermissionError.HTTPCode,
301                 response)
302
303         // unauthenticated /index/prefix request
304         // => PermissionError
305         response = IssueRequest(rest, unauth_prefix_req)
306         ExpectStatusCode(t,
307                 "enforce_permissions off, unauthenticated /index/prefix request",
308                 PermissionError.HTTPCode,
309                 response)
310
311         // authenticated /index request, non-superuser
312         // => PermissionError
313         response = IssueRequest(rest, authenticated_req)
314         ExpectStatusCode(t,
315                 "enforce_permissions off, authenticated request, non-superuser",
316                 PermissionError.HTTPCode,
317                 response)
318
319         // authenticated /index/prefix request, non-superuser
320         // => PermissionError
321         response = IssueRequest(rest, auth_prefix_req)
322         ExpectStatusCode(t,
323                 "enforce_permissions off, authenticated /index/prefix request, non-superuser",
324                 PermissionError.HTTPCode,
325                 response)
326
327         // authenticated /index request, superuser
328         // => PermissionError
329         response = IssueRequest(rest, superuser_req)
330         ExpectStatusCode(t,
331                 "enforce_permissions off, superuser request",
332                 PermissionError.HTTPCode,
333                 response)
334
335         // superuser /index/prefix request
336         // => PermissionError
337         response = IssueRequest(rest, superuser_prefix_req)
338         ExpectStatusCode(t,
339                 "enforce_permissions off, superuser /index/prefix request",
340                 PermissionError.HTTPCode,
341                 response)
342
343         // ---------------------------
344         // enforce_permissions enabled
345         // Only the superuser should be allowed to issue /index requests.
346         enforce_permissions = true
347
348         // unauthenticated /index request
349         // => PermissionError
350         response = IssueRequest(rest, unauthenticated_req)
351         ExpectStatusCode(t,
352                 "enforce_permissions on, unauthenticated request",
353                 PermissionError.HTTPCode,
354                 response)
355
356         // unauthenticated /index/prefix request
357         // => PermissionError
358         response = IssueRequest(rest, unauth_prefix_req)
359         ExpectStatusCode(t,
360                 "permissions on, unauthenticated /index/prefix request",
361                 PermissionError.HTTPCode,
362                 response)
363
364         // authenticated /index request, non-superuser
365         // => PermissionError
366         response = IssueRequest(rest, authenticated_req)
367         ExpectStatusCode(t,
368                 "permissions on, authenticated request, non-superuser",
369                 PermissionError.HTTPCode,
370                 response)
371
372         // authenticated /index/prefix request, non-superuser
373         // => PermissionError
374         response = IssueRequest(rest, auth_prefix_req)
375         ExpectStatusCode(t,
376                 "permissions on, authenticated /index/prefix request, non-superuser",
377                 PermissionError.HTTPCode,
378                 response)
379
380         // superuser /index request
381         // => OK
382         response = IssueRequest(rest, superuser_req)
383         ExpectStatusCode(t,
384                 "permissions on, superuser request",
385                 http.StatusOK,
386                 response)
387
388         expected := `^` + TEST_HASH + `\+\d+ \d+\n` +
389                 TEST_HASH_2 + `\+\d+ \d+\n$`
390         match, _ := regexp.MatchString(expected, response.Body.String())
391         if !match {
392                 t.Errorf(
393                         "permissions on, superuser request: expected %s, got:\n%s",
394                         expected, response.Body.String())
395         }
396
397         // superuser /index/prefix request
398         // => OK
399         response = IssueRequest(rest, superuser_prefix_req)
400         ExpectStatusCode(t,
401                 "permissions on, superuser request",
402                 http.StatusOK,
403                 response)
404
405         expected = `^` + TEST_HASH + `\+\d+ \d+\n$`
406         match, _ = regexp.MatchString(expected, response.Body.String())
407         if !match {
408                 t.Errorf(
409                         "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
410                         expected, response.Body.String())
411         }
412 }
413
414 // ====================
415 // Helper functions
416 // ====================
417
418 // IssueTestRequest executes an HTTP request described by rt, to a
419 // specified REST router.  It returns the HTTP response to the request.
420 func IssueRequest(router *mux.Router, rt *RequestTester) *httptest.ResponseRecorder {
421         response := httptest.NewRecorder()
422         body := bytes.NewReader(rt.request_body)
423         req, _ := http.NewRequest(rt.method, rt.uri, body)
424         if rt.api_token != "" {
425                 req.Header.Set("Authorization", "OAuth2 "+rt.api_token)
426         }
427         router.ServeHTTP(response, req)
428         return response
429 }
430
431 // ExpectStatusCode checks whether a response has the specified status code,
432 // and reports a test failure if not.
433 func ExpectStatusCode(
434         t *testing.T,
435         testname string,
436         expected_status int,
437         response *httptest.ResponseRecorder) {
438         if response.Code != expected_status {
439                 t.Errorf("%s: expected status %s, got %+v",
440                         testname, expected_status, response)
441         }
442 }
443
444 func ExpectBody(
445         t *testing.T,
446         testname string,
447         expected_body string,
448         response *httptest.ResponseRecorder) {
449         if response.Body.String() != expected_body {
450                 t.Errorf("%s: expected response body '%s', got %+v",
451                         testname, expected_body, response)
452         }
453 }