X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/cff5301bbc1e1c8f8421fb56dca5a76dfebb058c..03395937ba05b9e3192e346a355c691f45cc7c85:/services/keep/src/keep/handler_test.go diff --git a/services/keep/src/keep/handler_test.go b/services/keep/src/keep/handler_test.go new file mode 100644 index 0000000000..8e7bfea468 --- /dev/null +++ b/services/keep/src/keep/handler_test.go @@ -0,0 +1,438 @@ +// Tests for Keep HTTP handlers: +// +// GetBlockHandler +// PutBlockHandler +// IndexHandler +// +// The HTTP handlers are responsible for enforcing permission policy, +// so these tests must exercise all possible permission permutations. + +package main + +import ( + "bytes" + "github.com/gorilla/mux" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" +) + +// A RequestTester represents the parameters for an HTTP request to +// be issued on behalf of a unit test. +type RequestTester struct { + uri string + api_token string + method string + request_body []byte +} + +// Test GetBlockHandler on the following situations: +// - permissions off, unauthenticated request, unsigned locator +// - permissions on, authenticated request, signed locator +// - permissions on, authenticated request, unsigned locator +// - permissions on, unauthenticated request, signed locator +// - permissions on, authenticated request, expired locator +// +func TestGetHandler(t *testing.T) { + defer teardown() + + // Prepare two test Keep volumes. Our block is stored on the second volume. + KeepVM = MakeTestVolumeManager(2) + defer func() { KeepVM.Quit() }() + + vols := KeepVM.Volumes() + if err := vols[0].Put(TEST_HASH, TEST_BLOCK); err != nil { + t.Error(err) + } + + // Set up a REST router for testing the handlers. + rest := MakeRESTRouter() + + // Create locators for testing. + // Turn on permission settings so we can generate signed locators. + enforce_permissions = true + PermissionSecret = []byte(known_key) + permission_ttl = time.Duration(300) * time.Second + + var ( + unsigned_locator = "http://localhost:25107/" + TEST_HASH + valid_timestamp = time.Now().Add(permission_ttl) + expired_timestamp = time.Now().Add(-time.Hour) + signed_locator = "http://localhost:25107/" + SignLocator(TEST_HASH, known_token, valid_timestamp) + expired_locator = "http://localhost:25107/" + SignLocator(TEST_HASH, known_token, expired_timestamp) + ) + + // ----------------- + // Test unauthenticated request with permissions off. + enforce_permissions = false + + // Unauthenticated request, unsigned locator + // => OK + response := IssueRequest(rest, + &RequestTester{ + method: "GET", + uri: unsigned_locator, + }) + ExpectStatusCode(t, + "Unauthenticated request, unsigned locator", http.StatusOK, response) + ExpectBody(t, + "Unauthenticated request, unsigned locator", + string(TEST_BLOCK), + response) + + // ---------------- + // Permissions: on. + enforce_permissions = true + + // Authenticated request, signed locator + // => OK + response = IssueRequest(rest, &RequestTester{ + method: "GET", + uri: signed_locator, + api_token: known_token, + }) + ExpectStatusCode(t, + "Authenticated request, signed locator", http.StatusOK, response) + ExpectBody(t, + "Authenticated request, signed locator", string(TEST_BLOCK), response) + + // Authenticated request, unsigned locator + // => PermissionError + response = IssueRequest(rest, &RequestTester{ + method: "GET", + uri: unsigned_locator, + api_token: known_token, + }) + ExpectStatusCode(t, "unsigned locator", PermissionError.HTTPCode, response) + + // Unauthenticated request, signed locator + // => PermissionError + response = IssueRequest(rest, &RequestTester{ + method: "GET", + uri: signed_locator, + }) + ExpectStatusCode(t, + "Unauthenticated request, signed locator", + PermissionError.HTTPCode, response) + + // Authenticated request, expired locator + // => ExpiredError + response = IssueRequest(rest, &RequestTester{ + method: "GET", + uri: expired_locator, + api_token: known_token, + }) + ExpectStatusCode(t, + "Authenticated request, expired locator", + ExpiredError.HTTPCode, response) +} + +// Test PutBlockHandler on the following situations: +// - no server key +// - with server key, authenticated request, unsigned locator +// - with server key, unauthenticated request, unsigned locator +// +func TestPutHandler(t *testing.T) { + defer teardown() + + // Prepare two test Keep volumes. + KeepVM = MakeTestVolumeManager(2) + defer func() { KeepVM.Quit() }() + + // Set up a REST router for testing the handlers. + rest := MakeRESTRouter() + + // -------------- + // No server key. + + // Unauthenticated request, no server key + // => OK (unsigned response) + unsigned_locator := "http://localhost:25107/" + TEST_HASH + response := IssueRequest(rest, + &RequestTester{ + method: "PUT", + uri: unsigned_locator, + request_body: TEST_BLOCK, + }) + + ExpectStatusCode(t, + "Unauthenticated request, no server key", http.StatusOK, response) + ExpectBody(t, "Unauthenticated request, no server key", TEST_HASH, response) + + // ------------------ + // With a server key. + + PermissionSecret = []byte(known_key) + permission_ttl = time.Duration(300) * time.Second + + // When a permission key is available, the locator returned + // from an authenticated PUT request will be signed. + + // Authenticated PUT, signed locator + // => OK (signed response) + response = IssueRequest(rest, + &RequestTester{ + method: "PUT", + uri: unsigned_locator, + request_body: TEST_BLOCK, + api_token: known_token, + }) + + ExpectStatusCode(t, + "Authenticated PUT, signed locator, with server key", + http.StatusOK, response) + if !VerifySignature(response.Body.String(), known_token) { + t.Errorf("Authenticated PUT, signed locator, with server key:\n"+ + "response '%s' does not contain a valid signature", + response.Body.String()) + } + + // Unauthenticated PUT, unsigned locator + // => OK + response = IssueRequest(rest, + &RequestTester{ + method: "PUT", + uri: unsigned_locator, + request_body: TEST_BLOCK, + }) + + ExpectStatusCode(t, + "Unauthenticated PUT, unsigned locator, with server key", + http.StatusOK, response) + ExpectBody(t, + "Unauthenticated PUT, unsigned locator, with server key", + TEST_HASH, response) +} + +// Test /index requests: +// - enforce_permissions off | unauthenticated /index request +// - enforce_permissions off | unauthenticated /index/prefix request +// - enforce_permissions off | authenticated /index request | non-superuser +// - enforce_permissions off | authenticated /index/prefix request | non-superuser +// - enforce_permissions off | authenticated /index request | superuser +// - enforce_permissions off | authenticated /index/prefix request | superuser +// - enforce_permissions on | unauthenticated /index request +// - enforce_permissions on | unauthenticated /index/prefix request +// - enforce_permissions on | authenticated /index request | non-superuser +// - enforce_permissions on | authenticated /index/prefix request | non-superuser +// - enforce_permissions on | authenticated /index request | superuser +// - enforce_permissions on | authenticated /index/prefix request | superuser +// +// The only /index requests that should succeed are those issued by the +// superuser when enforce_permissions = true. +// +func TestIndexHandler(t *testing.T) { + defer teardown() + + // Set up Keep volumes and populate them. + // Include multiple blocks on different volumes, and + // some metadata files (which should be omitted from index listings) + KeepVM = MakeTestVolumeManager(2) + defer func() { KeepVM.Quit() }() + + vols := KeepVM.Volumes() + vols[0].Put(TEST_HASH, TEST_BLOCK) + vols[1].Put(TEST_HASH_2, TEST_BLOCK_2) + vols[0].Put(TEST_HASH+".meta", []byte("metadata")) + vols[1].Put(TEST_HASH_2+".meta", []byte("metadata")) + + // Set up a REST router for testing the handlers. + rest := MakeRESTRouter() + + data_manager_token = "DATA MANAGER TOKEN" + + unauthenticated_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index", + } + authenticated_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index", + api_token: known_token, + } + superuser_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index", + api_token: data_manager_token, + } + unauth_prefix_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index/" + TEST_HASH[0:3], + } + auth_prefix_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index/" + TEST_HASH[0:3], + api_token: known_token, + } + superuser_prefix_req := &RequestTester{ + method: "GET", + uri: "http://localhost:25107/index/" + TEST_HASH[0:3], + api_token: data_manager_token, + } + + // ---------------------------- + // enforce_permissions disabled + // All /index requests should fail. + enforce_permissions = false + + // unauthenticated /index request + // => PermissionError + response := IssueRequest(rest, unauthenticated_req) + ExpectStatusCode(t, + "enforce_permissions off, unauthenticated request", + PermissionError.HTTPCode, + response) + + // unauthenticated /index/prefix request + // => PermissionError + response = IssueRequest(rest, unauth_prefix_req) + ExpectStatusCode(t, + "enforce_permissions off, unauthenticated /index/prefix request", + PermissionError.HTTPCode, + response) + + // authenticated /index request, non-superuser + // => PermissionError + response = IssueRequest(rest, authenticated_req) + ExpectStatusCode(t, + "enforce_permissions off, authenticated request, non-superuser", + PermissionError.HTTPCode, + response) + + // authenticated /index/prefix request, non-superuser + // => PermissionError + response = IssueRequest(rest, auth_prefix_req) + ExpectStatusCode(t, + "enforce_permissions off, authenticated /index/prefix request, non-superuser", + PermissionError.HTTPCode, + response) + + // authenticated /index request, superuser + // => PermissionError + response = IssueRequest(rest, superuser_req) + ExpectStatusCode(t, + "enforce_permissions off, superuser request", + PermissionError.HTTPCode, + response) + + // superuser /index/prefix request + // => PermissionError + response = IssueRequest(rest, superuser_prefix_req) + ExpectStatusCode(t, + "enforce_permissions off, superuser /index/prefix request", + PermissionError.HTTPCode, + response) + + // --------------------------- + // enforce_permissions enabled + // Only the superuser should be allowed to issue /index requests. + enforce_permissions = true + + // unauthenticated /index request + // => PermissionError + response = IssueRequest(rest, unauthenticated_req) + ExpectStatusCode(t, + "enforce_permissions on, unauthenticated request", + PermissionError.HTTPCode, + response) + + // unauthenticated /index/prefix request + // => PermissionError + response = IssueRequest(rest, unauth_prefix_req) + ExpectStatusCode(t, + "permissions on, unauthenticated /index/prefix request", + PermissionError.HTTPCode, + response) + + // authenticated /index request, non-superuser + // => PermissionError + response = IssueRequest(rest, authenticated_req) + ExpectStatusCode(t, + "permissions on, authenticated request, non-superuser", + PermissionError.HTTPCode, + response) + + // authenticated /index/prefix request, non-superuser + // => PermissionError + response = IssueRequest(rest, auth_prefix_req) + ExpectStatusCode(t, + "permissions on, authenticated /index/prefix request, non-superuser", + PermissionError.HTTPCode, + response) + + // superuser /index request + // => OK + response = IssueRequest(rest, superuser_req) + ExpectStatusCode(t, + "permissions on, superuser request", + http.StatusOK, + response) + + expected := `^` + TEST_HASH + `\+\d+ \d+\n` + + TEST_HASH_2 + `\+\d+ \d+\n$` + match, _ := regexp.MatchString(expected, response.Body.String()) + if !match { + t.Errorf( + "permissions on, superuser request: expected %s, got:\n%s", + expected, response.Body.String()) + } + + // superuser /index/prefix request + // => OK + response = IssueRequest(rest, superuser_prefix_req) + ExpectStatusCode(t, + "permissions on, superuser request", + http.StatusOK, + response) + + expected = `^` + TEST_HASH + `\+\d+ \d+\n$` + match, _ = regexp.MatchString(expected, response.Body.String()) + if !match { + t.Errorf( + "permissions on, superuser /index/prefix request: expected %s, got:\n%s", + expected, response.Body.String()) + } +} + +// ==================== +// Helper functions +// ==================== + +// IssueTestRequest executes an HTTP request described by rt, to a +// specified REST router. It returns the HTTP response to the request. +func IssueRequest(router *mux.Router, rt *RequestTester) *httptest.ResponseRecorder { + response := httptest.NewRecorder() + body := bytes.NewReader(rt.request_body) + req, _ := http.NewRequest(rt.method, rt.uri, body) + if rt.api_token != "" { + req.Header.Set("Authorization", "OAuth "+rt.api_token) + } + router.ServeHTTP(response, req) + return response +} + +// ExpectStatusCode checks whether a response has the specified status code, +// and reports a test failure if not. +func ExpectStatusCode( + t *testing.T, + testname string, + expected_status int, + response *httptest.ResponseRecorder) { + if response.Code != expected_status { + t.Errorf("%s: expected status %s, got %+v", + testname, expected_status, response) + } +} + +func ExpectBody( + t *testing.T, + testname string, + expected_body string, + response *httptest.ResponseRecorder) { + if response.Body.String() != expected_body { + t.Errorf("%s: expected response body '%s', got %+v", + testname, expected_body, response) + } +}