1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
23 "git.arvados.org/arvados.git/lib/config"
24 "git.arvados.org/arvados.git/sdk/go/arvados"
25 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
26 "git.arvados.org/arvados.git/sdk/go/arvadostest"
27 "git.arvados.org/arvados.git/sdk/go/auth"
28 "git.arvados.org/arvados.git/sdk/go/ctxlog"
29 "git.arvados.org/arvados.git/sdk/go/keepclient"
30 "github.com/sirupsen/logrus"
31 check "gopkg.in/check.v1"
34 var _ = check.Suite(&UnitSuite{})
37 arvados.DebugLocksPanicMode = true
40 type UnitSuite struct {
41 Config *arvados.Config
44 func (s *UnitSuite) SetUpTest(c *check.C) {
45 ldr := config.NewLoader(bytes.NewBufferString("Clusters: {zzzzz: {}}"), ctxlog.TestLogger(c))
47 cfg, err := ldr.Load()
48 c.Assert(err, check.IsNil)
52 func (s *UnitSuite) TestCORSPreflight(c *check.C) {
53 h := handler{Config: newConfig(ctxlog.TestLogger(c), s.Config)}
54 u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/foo")
59 RequestURI: u.RequestURI(),
61 "Origin": {"https://workbench.example"},
62 "Access-Control-Request-Method": {"POST"},
66 // Check preflight for an allowed request
67 resp := httptest.NewRecorder()
68 h.ServeHTTP(resp, req)
69 c.Check(resp.Code, check.Equals, http.StatusOK)
70 c.Check(resp.Body.String(), check.Equals, "")
71 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
72 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "COPY, DELETE, GET, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, RMCOL, UNLOCK")
73 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Authorization, Content-Type, Range, Depth, Destination, If, Lock-Token, Overwrite, Timeout")
75 // Check preflight for a disallowed request
76 resp = httptest.NewRecorder()
77 req.Header.Set("Access-Control-Request-Method", "MAKE-COFFEE")
78 h.ServeHTTP(resp, req)
79 c.Check(resp.Body.String(), check.Equals, "")
80 c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
83 func (s *UnitSuite) TestEmptyResponse(c *check.C) {
84 for _, trial := range []struct {
90 // If we return no content due to a Keep read error,
91 // we should emit a log message.
92 {false, false, http.StatusOK, `(?ms).*only wrote 0 bytes.*`},
94 // If we return no content because the client sent an
95 // If-Modified-Since header, our response should be
96 // 304. We still expect a "File download" log since it
97 // counts as a file access for auditing.
98 {true, true, http.StatusNotModified, `(?ms).*msg="File download".*`},
100 c.Logf("trial: %+v", trial)
101 arvadostest.StartKeep(2, true)
102 if trial.dataExists {
103 arv, err := arvadosclient.MakeArvadosClient()
104 c.Assert(err, check.IsNil)
105 arv.ApiToken = arvadostest.ActiveToken
106 kc, err := keepclient.MakeKeepClient(arv)
107 c.Assert(err, check.IsNil)
108 _, _, err = kc.PutB([]byte("foo"))
109 c.Assert(err, check.IsNil)
112 h := handler{Config: newConfig(ctxlog.TestLogger(c), s.Config)}
113 u := mustParseURL("http://" + arvadostest.FooCollection + ".keep-web.example/foo")
114 req := &http.Request{
118 RequestURI: u.RequestURI(),
120 "Authorization": {"Bearer " + arvadostest.ActiveToken},
123 if trial.sendIMSHeader {
124 req.Header.Set("If-Modified-Since", strings.Replace(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT", -1))
127 var logbuf bytes.Buffer
128 logger := logrus.New()
130 req = req.WithContext(ctxlog.Context(context.Background(), logger))
132 resp := httptest.NewRecorder()
133 h.ServeHTTP(resp, req)
134 c.Check(resp.Code, check.Equals, trial.expectStatus)
135 c.Check(resp.Body.String(), check.Equals, "")
137 c.Log(logbuf.String())
138 c.Check(logbuf.String(), check.Matches, trial.logRegexp)
142 func (s *UnitSuite) TestInvalidUUID(c *check.C) {
143 bogusID := strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + "-"
144 token := arvadostest.ActiveToken
145 for _, trial := range []string{
146 "http://keep-web/c=" + bogusID + "/foo",
147 "http://keep-web/c=" + bogusID + "/t=" + token + "/foo",
148 "http://keep-web/collections/download/" + bogusID + "/" + token + "/foo",
149 "http://keep-web/collections/" + bogusID + "/foo",
150 "http://" + bogusID + ".keep-web/" + bogusID + "/foo",
151 "http://" + bogusID + ".keep-web/t=" + token + "/" + bogusID + "/foo",
154 u := mustParseURL(trial)
155 req := &http.Request{
159 RequestURI: u.RequestURI(),
161 resp := httptest.NewRecorder()
162 cfg := newConfig(ctxlog.TestLogger(c), s.Config)
163 cfg.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
164 h := handler{Config: cfg}
165 h.ServeHTTP(resp, req)
166 c.Check(resp.Code, check.Equals, http.StatusNotFound)
170 func mustParseURL(s string) *url.URL {
171 r, err := url.Parse(s)
173 panic("parse URL: " + s)
178 func (s *IntegrationSuite) TestVhost404(c *check.C) {
179 for _, testURL := range []string{
180 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
181 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
183 resp := httptest.NewRecorder()
184 u := mustParseURL(testURL)
185 req := &http.Request{
188 RequestURI: u.RequestURI(),
190 s.testServer.Handler.ServeHTTP(resp, req)
191 c.Check(resp.Code, check.Equals, http.StatusNotFound)
192 c.Check(resp.Body.String(), check.Equals, notFoundMessage+"\n")
196 // An authorizer modifies an HTTP request to make use of the given
197 // token -- by adding it to a header, cookie, query param, or whatever
198 // -- and returns the HTTP status code we should expect from keep-web if
199 // the token is invalid.
200 type authorizer func(*http.Request, string) int
202 func (s *IntegrationSuite) TestVhostViaAuthzHeaderOAuth2(c *check.C) {
203 s.doVhostRequests(c, authzViaAuthzHeaderOAuth2)
205 func authzViaAuthzHeaderOAuth2(r *http.Request, tok string) int {
206 r.Header.Add("Authorization", "Bearer "+tok)
207 return http.StatusUnauthorized
209 func (s *IntegrationSuite) TestVhostViaAuthzHeaderBearer(c *check.C) {
210 s.doVhostRequests(c, authzViaAuthzHeaderBearer)
212 func authzViaAuthzHeaderBearer(r *http.Request, tok string) int {
213 r.Header.Add("Authorization", "Bearer "+tok)
214 return http.StatusUnauthorized
217 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
218 s.doVhostRequests(c, authzViaCookieValue)
220 func authzViaCookieValue(r *http.Request, tok string) int {
221 r.AddCookie(&http.Cookie{
222 Name: "arvados_api_token",
223 Value: auth.EncodeTokenCookie([]byte(tok)),
225 return http.StatusUnauthorized
228 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
229 s.doVhostRequests(c, authzViaPath)
231 func authzViaPath(r *http.Request, tok string) int {
232 r.URL.Path = "/t=" + tok + r.URL.Path
233 return http.StatusNotFound
236 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
237 s.doVhostRequests(c, authzViaQueryString)
239 func authzViaQueryString(r *http.Request, tok string) int {
240 r.URL.RawQuery = "api_token=" + tok
241 return http.StatusUnauthorized
244 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
245 s.doVhostRequests(c, authzViaPOST)
247 func authzViaPOST(r *http.Request, tok string) int {
249 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
250 r.Body = ioutil.NopCloser(strings.NewReader(
251 url.Values{"api_token": {tok}}.Encode()))
252 return http.StatusUnauthorized
255 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
256 s.doVhostRequests(c, authzViaPOST)
258 func authzViaXHRPOST(r *http.Request, tok string) int {
260 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
261 r.Header.Add("Origin", "https://origin.example")
262 r.Body = ioutil.NopCloser(strings.NewReader(
265 "disposition": {"attachment"},
267 return http.StatusUnauthorized
270 // Try some combinations of {url, token} using the given authorization
271 // mechanism, and verify the result is correct.
272 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
273 for _, hostPath := range []string{
274 arvadostest.FooCollection + ".example.com/foo",
275 arvadostest.FooCollection + "--collections.example.com/foo",
276 arvadostest.FooCollection + "--collections.example.com/_/foo",
277 arvadostest.FooCollectionPDH + ".example.com/foo",
278 strings.Replace(arvadostest.FooCollectionPDH, "+", "-", -1) + "--collections.example.com/foo",
279 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
281 c.Log("doRequests: ", hostPath)
282 s.doVhostRequestsWithHostPath(c, authz, hostPath)
286 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
287 for _, tok := range []string{
288 arvadostest.ActiveToken,
289 arvadostest.ActiveToken[:15],
290 arvadostest.SpectatorToken,
294 u := mustParseURL("http://" + hostPath)
295 req := &http.Request{
299 RequestURI: u.RequestURI(),
300 Header: http.Header{},
302 failCode := authz(req, tok)
303 req, resp := s.doReq(req)
304 code, body := resp.Code, resp.Body.String()
306 // If the initial request had a (non-empty) token
307 // showing in the query string, we should have been
308 // redirected in order to hide it in a cookie.
309 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
311 if tok == arvadostest.ActiveToken {
312 c.Check(code, check.Equals, http.StatusOK)
313 c.Check(body, check.Equals, "foo")
315 c.Check(code >= 400, check.Equals, true)
316 c.Check(code < 500, check.Equals, true)
317 if tok == arvadostest.SpectatorToken {
318 // Valid token never offers to retry
319 // with different credentials.
320 c.Check(code, check.Equals, http.StatusNotFound)
322 // Invalid token can ask to retry
323 // depending on the authz method.
324 c.Check(code, check.Equals, failCode)
327 c.Check(body, check.Equals, notFoundMessage+"\n")
329 c.Check(body, check.Equals, unauthorizedMessage+"\n")
335 func (s *IntegrationSuite) TestVhostPortMatch(c *check.C) {
336 for _, host := range []string{"download.example.com", "DOWNLOAD.EXAMPLE.COM"} {
337 for _, port := range []string{"80", "443", "8000"} {
338 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = fmt.Sprintf("download.example.com:%v", port)
339 u := mustParseURL(fmt.Sprintf("http://%v/by_id/%v/foo", host, arvadostest.FooCollection))
340 req := &http.Request{
344 RequestURI: u.RequestURI(),
345 Header: http.Header{"Authorization": []string{"Bearer " + arvadostest.ActiveToken}},
347 req, resp := s.doReq(req)
348 code, _ := resp.Code, resp.Body.String()
351 c.Check(code, check.Equals, 401)
353 c.Check(code, check.Equals, 200)
359 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
360 resp := httptest.NewRecorder()
361 s.testServer.Handler.ServeHTTP(resp, req)
362 if resp.Code != http.StatusSeeOther {
365 cookies := (&http.Response{Header: resp.Header()}).Cookies()
366 u, _ := req.URL.Parse(resp.Header().Get("Location"))
371 RequestURI: u.RequestURI(),
372 Header: http.Header{},
374 for _, c := range cookies {
380 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
381 s.testVhostRedirectTokenToCookie(c, "GET",
382 arvadostest.FooCollection+".example.com/foo",
383 "?api_token="+arvadostest.ActiveToken,
391 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
392 s.testVhostRedirectTokenToCookie(c, "GET",
393 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
402 // Bad token in URL is 404 Not Found because it doesn't make sense to
403 // retry the same URL with different authorization.
404 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
405 s.testVhostRedirectTokenToCookie(c, "GET",
406 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
411 notFoundMessage+"\n",
415 // Bad token in a cookie (even if it got there via our own
416 // query-string-to-cookie redirect) is, in principle, retryable at the
417 // same URL so it's 401 Unauthorized.
418 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
419 s.testVhostRedirectTokenToCookie(c, "GET",
420 arvadostest.FooCollection+".example.com/foo",
421 "?api_token=thisisabogustoken",
424 http.StatusUnauthorized,
425 unauthorizedMessage+"\n",
429 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
430 s.testVhostRedirectTokenToCookie(c, "GET",
431 "example.com/c="+arvadostest.FooCollection+"/foo",
432 "?api_token="+arvadostest.ActiveToken,
435 http.StatusBadRequest,
436 "cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n",
440 // If client requests an attachment by putting ?disposition=attachment
441 // in the query string, and gets redirected, the redirect target
442 // should respond with an attachment.
443 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
444 resp := s.testVhostRedirectTokenToCookie(c, "GET",
445 arvadostest.FooCollection+".example.com/foo",
446 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
452 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
455 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
456 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
457 resp := s.testVhostRedirectTokenToCookie(c, "GET",
458 "download.example.com/by_id/"+arvadostest.FooCollection+"/foo",
459 "?api_token="+arvadostest.ActiveToken,
465 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
468 func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
469 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
470 resp := s.testVhostRedirectTokenToCookie(c, "GET",
471 "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
472 "?api_token="+arvadostest.ActiveToken,
478 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
479 resp = s.testVhostRedirectTokenToCookie(c, "GET",
480 "download.example.com/by_id/"+arvadostest.WazVersion1Collection+"/waz",
481 "?api_token="+arvadostest.ActiveToken,
487 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
490 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
491 s.testServer.Config.cluster.Collections.TrustAllContent = true
492 s.testVhostRedirectTokenToCookie(c, "GET",
493 "example.com/c="+arvadostest.FooCollection+"/foo",
494 "?api_token="+arvadostest.ActiveToken,
502 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
503 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com:1234"
505 s.testVhostRedirectTokenToCookie(c, "GET",
506 "example.com/c="+arvadostest.FooCollection+"/foo",
507 "?api_token="+arvadostest.ActiveToken,
510 http.StatusBadRequest,
511 "cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n",
514 resp := s.testVhostRedirectTokenToCookie(c, "GET",
515 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
516 "?api_token="+arvadostest.ActiveToken,
522 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
525 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
526 s.testVhostRedirectTokenToCookie(c, "POST",
527 arvadostest.FooCollection+".example.com/foo",
529 "application/x-www-form-urlencoded",
530 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
536 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
537 s.testVhostRedirectTokenToCookie(c, "POST",
538 arvadostest.FooCollection+".example.com/foo",
540 "application/x-www-form-urlencoded",
541 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
543 notFoundMessage+"\n",
547 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
548 s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
549 s.testVhostRedirectTokenToCookie(c, "GET",
550 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
559 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
560 s.testServer.Config.cluster.Users.AnonymousUserToken = "anonymousTokenConfiguredButInvalid"
561 s.testVhostRedirectTokenToCookie(c, "GET",
562 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
567 notFoundMessage+"\n",
571 func (s *IntegrationSuite) TestSpecialCharsInPath(c *check.C) {
572 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
574 client := s.testServer.Config.Client
575 client.AuthToken = arvadostest.ActiveToken
576 fs, err := (&arvados.Collection{}).FileSystem(&client, nil)
577 c.Assert(err, check.IsNil)
578 f, err := fs.OpenFile("https:\\\"odd' path chars", os.O_CREATE, 0777)
579 c.Assert(err, check.IsNil)
581 mtxt, err := fs.MarshalManifest(".")
582 c.Assert(err, check.IsNil)
583 var coll arvados.Collection
584 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
585 "collection": map[string]string{
586 "manifest_text": mtxt,
589 c.Assert(err, check.IsNil)
591 u, _ := url.Parse("http://download.example.com/c=" + coll.UUID + "/")
592 req := &http.Request{
596 RequestURI: u.RequestURI(),
598 "Authorization": {"Bearer " + client.AuthToken},
601 resp := httptest.NewRecorder()
602 s.testServer.Handler.ServeHTTP(resp, req)
603 c.Check(resp.Code, check.Equals, http.StatusOK)
604 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./https:%5c%22odd%27%20path%20chars"\S+https:\\"odd' path chars.*`)
607 func (s *IntegrationSuite) TestForwardSlashSubstitution(c *check.C) {
608 arv := arvados.NewClientFromEnv()
609 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
610 s.testServer.Config.cluster.Collections.ForwardSlashNameSubstitution = "{SOLIDUS}"
611 name := "foo/bar/baz"
612 nameShown := strings.Replace(name, "/", "{SOLIDUS}", -1)
613 nameShownEscaped := strings.Replace(name, "/", "%7bSOLIDUS%7d", -1)
615 client := s.testServer.Config.Client
616 client.AuthToken = arvadostest.ActiveToken
617 fs, err := (&arvados.Collection{}).FileSystem(&client, nil)
618 c.Assert(err, check.IsNil)
619 f, err := fs.OpenFile("filename", os.O_CREATE, 0777)
620 c.Assert(err, check.IsNil)
622 mtxt, err := fs.MarshalManifest(".")
623 c.Assert(err, check.IsNil)
624 var coll arvados.Collection
625 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
626 "collection": map[string]string{
627 "manifest_text": mtxt,
629 "owner_uuid": arvadostest.AProjectUUID,
632 c.Assert(err, check.IsNil)
633 defer arv.RequestAndDecode(&coll, "DELETE", "arvados/v1/collections/"+coll.UUID, nil, nil)
635 base := "http://download.example.com/by_id/" + coll.OwnerUUID + "/"
636 for tryURL, expectRegexp := range map[string]string{
637 base: `(?ms).*href="./` + nameShownEscaped + `/"\S+` + nameShown + `.*`,
638 base + nameShownEscaped + "/": `(?ms).*href="./filename"\S+filename.*`,
640 u, _ := url.Parse(tryURL)
641 req := &http.Request{
645 RequestURI: u.RequestURI(),
647 "Authorization": {"Bearer " + client.AuthToken},
650 resp := httptest.NewRecorder()
651 s.testServer.Handler.ServeHTTP(resp, req)
652 c.Check(resp.Code, check.Equals, http.StatusOK)
653 c.Check(resp.Body.String(), check.Matches, expectRegexp)
657 // XHRs can't follow redirect-with-cookie so they rely on method=POST
658 // and disposition=attachment (telling us it's acceptable to respond
659 // with content instead of a redirect) and an Origin header that gets
660 // added automatically by the browser (telling us it's desirable to do
662 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
663 u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
664 req := &http.Request{
668 RequestURI: u.RequestURI(),
670 "Origin": {"https://origin.example"},
671 "Content-Type": {"application/x-www-form-urlencoded"},
673 Body: ioutil.NopCloser(strings.NewReader(url.Values{
674 "api_token": {arvadostest.ActiveToken},
675 "disposition": {"attachment"},
678 resp := httptest.NewRecorder()
679 s.testServer.Handler.ServeHTTP(resp, req)
680 c.Check(resp.Code, check.Equals, http.StatusOK)
681 c.Check(resp.Body.String(), check.Equals, "foo")
682 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
684 // GET + Origin header is representative of both AJAX GET
685 // requests and inline images via <IMG crossorigin="anonymous"
687 u.RawQuery = "api_token=" + url.QueryEscape(arvadostest.ActiveTokenV2)
692 RequestURI: u.RequestURI(),
694 "Origin": {"https://origin.example"},
697 resp = httptest.NewRecorder()
698 s.testServer.Handler.ServeHTTP(resp, req)
699 c.Check(resp.Code, check.Equals, http.StatusOK)
700 c.Check(resp.Body.String(), check.Equals, "foo")
701 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
704 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
705 u, _ := url.Parse(`http://` + hostPath + queryString)
706 req := &http.Request{
710 RequestURI: u.RequestURI(),
711 Header: http.Header{"Content-Type": {contentType}},
712 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
715 resp := httptest.NewRecorder()
717 c.Check(resp.Code, check.Equals, expectStatus)
718 c.Check(resp.Body.String(), check.Equals, expectRespBody)
721 s.testServer.Handler.ServeHTTP(resp, req)
722 if resp.Code != http.StatusSeeOther {
725 c.Check(resp.Body.String(), check.Matches, `.*href="http://`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
726 cookies := (&http.Response{Header: resp.Header()}).Cookies()
728 u, _ = u.Parse(resp.Header().Get("Location"))
733 RequestURI: u.RequestURI(),
734 Header: http.Header{},
736 for _, c := range cookies {
740 resp = httptest.NewRecorder()
741 s.testServer.Handler.ServeHTTP(resp, req)
742 c.Check(resp.Header().Get("Location"), check.Equals, "")
746 func (s *IntegrationSuite) TestDirectoryListingWithAnonymousToken(c *check.C) {
747 s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
748 s.testDirectoryListing(c)
751 func (s *IntegrationSuite) TestDirectoryListingWithNoAnonymousToken(c *check.C) {
752 s.testServer.Config.cluster.Users.AnonymousUserToken = ""
753 s.testDirectoryListing(c)
756 func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
757 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
758 authHeader := http.Header{
759 "Authorization": {"OAuth2 " + arvadostest.ActiveToken},
761 for _, trial := range []struct {
769 uri: strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/",
771 expect: []string{"dir1/foo", "dir1/bar"},
775 uri: strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/dir1/",
777 expect: []string{"foo", "bar"},
781 // URLs of this form ignore authHeader, and
782 // FooAndBarFilesInDirUUID isn't public, so
784 uri: "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
789 uri: "download.example.com/users/active/foo_file_in_dir/",
791 expect: []string{"dir1/"},
795 uri: "download.example.com/users/active/foo_file_in_dir/dir1/",
797 expect: []string{"bar"},
801 uri: "download.example.com/",
803 expect: []string{"users/"},
807 uri: "download.example.com/users",
810 expect: []string{"active/"},
814 uri: "download.example.com/users/",
816 expect: []string{"active/"},
820 uri: "download.example.com/users/active",
822 redirect: "/users/active/",
823 expect: []string{"foo_file_in_dir/"},
827 uri: "download.example.com/users/active/",
829 expect: []string{"foo_file_in_dir/"},
833 uri: "collections.example.com/collections/download/" + arvadostest.FooAndBarFilesInDirUUID + "/" + arvadostest.ActiveToken + "/",
835 expect: []string{"dir1/foo", "dir1/bar"},
839 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/",
841 expect: []string{"dir1/foo", "dir1/bar"},
845 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken,
847 expect: []string{"dir1/foo", "dir1/bar"},
851 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID,
853 expect: []string{"dir1/foo", "dir1/bar"},
857 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1",
859 redirect: "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1/",
860 expect: []string{"foo", "bar"},
864 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/_/dir1/",
866 expect: []string{"foo", "bar"},
870 uri: arvadostest.FooAndBarFilesInDirUUID + ".example.com/dir1?api_token=" + arvadostest.ActiveToken,
873 expect: []string{"foo", "bar"},
877 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/theperthcountyconspiracydoesnotexist/",
882 uri: "download.example.com/c=" + arvadostest.WazVersion1Collection,
884 expect: []string{"waz"},
888 uri: "download.example.com/by_id/" + arvadostest.WazVersion1Collection,
890 expect: []string{"waz"},
894 comment := check.Commentf("HTML: %q => %q", trial.uri, trial.expect)
895 resp := httptest.NewRecorder()
896 u := mustParseURL("//" + trial.uri)
897 req := &http.Request{
901 RequestURI: u.RequestURI(),
902 Header: copyHeader(trial.header),
904 s.testServer.Handler.ServeHTTP(resp, req)
905 var cookies []*http.Cookie
906 for resp.Code == http.StatusSeeOther {
907 u, _ := req.URL.Parse(resp.Header().Get("Location"))
912 RequestURI: u.RequestURI(),
913 Header: copyHeader(trial.header),
915 cookies = append(cookies, (&http.Response{Header: resp.Header()}).Cookies()...)
916 for _, c := range cookies {
919 resp = httptest.NewRecorder()
920 s.testServer.Handler.ServeHTTP(resp, req)
922 if trial.redirect != "" {
923 c.Check(req.URL.Path, check.Equals, trial.redirect, comment)
925 if trial.expect == nil {
926 if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
927 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
929 c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
932 c.Check(resp.Code, check.Equals, http.StatusOK, comment)
933 for _, e := range trial.expect {
934 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./`+e+`".*`, comment)
936 c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`, comment)
939 comment = check.Commentf("WebDAV: %q => %q", trial.uri, trial.expect)
944 RequestURI: u.RequestURI(),
945 Header: copyHeader(trial.header),
946 Body: ioutil.NopCloser(&bytes.Buffer{}),
948 resp = httptest.NewRecorder()
949 s.testServer.Handler.ServeHTTP(resp, req)
950 if trial.expect == nil {
951 if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
952 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
954 c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
957 c.Check(resp.Code, check.Equals, http.StatusOK, comment)
964 RequestURI: u.RequestURI(),
965 Header: copyHeader(trial.header),
966 Body: ioutil.NopCloser(&bytes.Buffer{}),
968 resp = httptest.NewRecorder()
969 s.testServer.Handler.ServeHTTP(resp, req)
970 if trial.expect == nil {
971 if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
972 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
974 c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
977 c.Check(resp.Code, check.Equals, http.StatusMultiStatus, comment)
978 for _, e := range trial.expect {
979 if strings.HasSuffix(e, "/") {
980 e = filepath.Join(u.Path, e) + "/"
982 e = filepath.Join(u.Path, e)
984 c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+e+`</D:href>.*`, comment)
990 func (s *IntegrationSuite) TestDeleteLastFile(c *check.C) {
991 arv := arvados.NewClientFromEnv()
992 var newCollection arvados.Collection
993 err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
994 "collection": map[string]string{
995 "owner_uuid": arvadostest.ActiveUserUUID,
996 "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
997 "name": "keep-web test collection",
999 "ensure_unique_name": true,
1001 c.Assert(err, check.IsNil)
1002 defer arv.RequestAndDecode(&newCollection, "DELETE", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
1004 var updated arvados.Collection
1005 for _, fnm := range []string{"foo.txt", "bar.txt"} {
1006 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com"
1007 u, _ := url.Parse("http://example.com/c=" + newCollection.UUID + "/" + fnm)
1008 req := &http.Request{
1012 RequestURI: u.RequestURI(),
1013 Header: http.Header{
1014 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1017 resp := httptest.NewRecorder()
1018 s.testServer.Handler.ServeHTTP(resp, req)
1019 c.Check(resp.Code, check.Equals, http.StatusNoContent)
1021 updated = arvados.Collection{}
1022 err = arv.RequestAndDecode(&updated, "GET", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
1023 c.Check(err, check.IsNil)
1024 c.Check(updated.ManifestText, check.Not(check.Matches), `(?ms).*\Q`+fnm+`\E.*`)
1025 c.Logf("updated manifest_text %q", updated.ManifestText)
1027 c.Check(updated.ManifestText, check.Equals, "")
1030 func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) {
1031 s.testServer.Config.cluster.ManagementToken = arvadostest.ManagementToken
1032 authHeader := http.Header{
1033 "Authorization": {"Bearer " + arvadostest.ManagementToken},
1036 resp := httptest.NewRecorder()
1037 u := mustParseURL("http://download.example.com/_health/ping")
1038 req := &http.Request{
1042 RequestURI: u.RequestURI(),
1045 s.testServer.Handler.ServeHTTP(resp, req)
1047 c.Check(resp.Code, check.Equals, http.StatusOK)
1048 c.Check(resp.Body.String(), check.Matches, `{"health":"OK"}\n`)
1051 func (s *IntegrationSuite) TestFileContentType(c *check.C) {
1052 s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
1054 client := s.testServer.Config.Client
1055 client.AuthToken = arvadostest.ActiveToken
1056 arv, err := arvadosclient.New(&client)
1057 c.Assert(err, check.Equals, nil)
1058 kc, err := keepclient.MakeKeepClient(arv)
1059 c.Assert(err, check.Equals, nil)
1061 fs, err := (&arvados.Collection{}).FileSystem(&client, kc)
1062 c.Assert(err, check.IsNil)
1064 trials := []struct {
1069 {"picture.txt", "BMX bikes are small this year\n", "text/plain; charset=utf-8"},
1070 {"picture.bmp", "BMX bikes are small this year\n", "image/(x-ms-)?bmp"},
1071 {"picture.jpg", "BMX bikes are small this year\n", "image/jpeg"},
1072 {"picture1", "BMX bikes are small this year\n", "image/bmp"}, // content sniff; "BM" is the magic signature for .bmp
1073 {"picture2", "Cars are small this year\n", "text/plain; charset=utf-8"}, // content sniff
1075 for _, trial := range trials {
1076 f, err := fs.OpenFile(trial.filename, os.O_CREATE|os.O_WRONLY, 0777)
1077 c.Assert(err, check.IsNil)
1078 _, err = f.Write([]byte(trial.content))
1079 c.Assert(err, check.IsNil)
1080 c.Assert(f.Close(), check.IsNil)
1082 mtxt, err := fs.MarshalManifest(".")
1083 c.Assert(err, check.IsNil)
1084 var coll arvados.Collection
1085 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
1086 "collection": map[string]string{
1087 "manifest_text": mtxt,
1090 c.Assert(err, check.IsNil)
1092 for _, trial := range trials {
1093 u, _ := url.Parse("http://download.example.com/by_id/" + coll.UUID + "/" + trial.filename)
1094 req := &http.Request{
1098 RequestURI: u.RequestURI(),
1099 Header: http.Header{
1100 "Authorization": {"Bearer " + client.AuthToken},
1103 resp := httptest.NewRecorder()
1104 s.testServer.Handler.ServeHTTP(resp, req)
1105 c.Check(resp.Code, check.Equals, http.StatusOK)
1106 c.Check(resp.Header().Get("Content-Type"), check.Matches, trial.contentType)
1107 c.Check(resp.Body.String(), check.Equals, trial.content)
1111 func (s *IntegrationSuite) TestKeepClientBlockCache(c *check.C) {
1112 s.testServer.Config.cluster.Collections.WebDAVCache.MaxBlockEntries = 42
1113 c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Not(check.Equals), 42)
1114 u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/t=" + arvadostest.ActiveToken + "/foo")
1115 req := &http.Request{
1119 RequestURI: u.RequestURI(),
1121 resp := httptest.NewRecorder()
1122 s.testServer.Handler.ServeHTTP(resp, req)
1123 c.Check(resp.Code, check.Equals, http.StatusOK)
1124 c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Equals, 42)
1127 // Writing to a collection shouldn't affect its entry in the
1128 // PDH-to-manifest cache.
1129 func (s *IntegrationSuite) TestCacheWriteCollectionSamePDH(c *check.C) {
1130 arv, err := arvadosclient.MakeArvadosClient()
1131 c.Assert(err, check.Equals, nil)
1132 arv.ApiToken = arvadostest.ActiveToken
1134 u := mustParseURL("http://x.example/testfile")
1135 req := &http.Request{
1139 RequestURI: u.RequestURI(),
1140 Header: http.Header{"Authorization": {"Bearer " + arv.ApiToken}},
1143 checkWithID := func(id string, status int) {
1144 req.URL.Host = strings.Replace(id, "+", "-", -1) + ".example"
1145 req.Host = req.URL.Host
1146 resp := httptest.NewRecorder()
1147 s.testServer.Handler.ServeHTTP(resp, req)
1148 c.Check(resp.Code, check.Equals, status)
1151 var colls [2]arvados.Collection
1152 for i := range colls {
1153 err := arv.Create("collections",
1154 map[string]interface{}{
1155 "ensure_unique_name": true,
1156 "collection": map[string]interface{}{
1157 "name": "test collection",
1160 c.Assert(err, check.Equals, nil)
1163 // Populate cache with empty collection
1164 checkWithID(colls[0].PortableDataHash, http.StatusNotFound)
1166 // write a file to colls[0]
1168 reqPut.Method = "PUT"
1169 reqPut.URL.Host = colls[0].UUID + ".example"
1170 reqPut.Host = req.URL.Host
1171 reqPut.Body = ioutil.NopCloser(bytes.NewBufferString("testdata"))
1172 resp := httptest.NewRecorder()
1173 s.testServer.Handler.ServeHTTP(resp, &reqPut)
1174 c.Check(resp.Code, check.Equals, http.StatusCreated)
1176 // new file should not appear in colls[1]
1177 checkWithID(colls[1].PortableDataHash, http.StatusNotFound)
1178 checkWithID(colls[1].UUID, http.StatusNotFound)
1180 checkWithID(colls[0].UUID, http.StatusOK)
1183 func copyHeader(h http.Header) http.Header {
1185 for k, v := range h {
1186 hc[k] = append([]string(nil), v...)
1191 func (s *IntegrationSuite) checkUploadDownloadRequest(c *check.C, h *handler, req *http.Request,
1192 successCode int, direction string, perm bool, userUuid string, collectionUuid string, filepath string) {
1194 client := s.testServer.Config.Client
1195 client.AuthToken = arvadostest.AdminToken
1196 var logentries arvados.LogList
1198 err := client.RequestAndDecode(&logentries, "GET", "arvados/v1/logs", nil,
1199 arvados.ResourceListParams{
1201 Order: "created_at desc"})
1202 c.Check(err, check.IsNil)
1203 c.Check(logentries.Items, check.HasLen, 1)
1204 lastLogId := logentries.Items[0].ID
1206 var logbuf bytes.Buffer
1207 logger := logrus.New()
1208 logger.Out = &logbuf
1209 resp := httptest.NewRecorder()
1210 req = req.WithContext(ctxlog.Context(context.Background(), logger))
1211 h.ServeHTTP(resp, req)
1214 c.Check(resp.Result().StatusCode, check.Equals, successCode)
1215 c.Check(logbuf.String(), check.Matches, `(?ms).*msg="File `+direction+`".*`)
1216 c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*level=error.*`)
1218 deadline := time.Now().Add(time.Second)
1220 c.Assert(time.Now().After(deadline), check.Equals, false, check.Commentf("timed out waiting for log entry"))
1221 err = client.RequestAndDecode(&logentries, "GET", "arvados/v1/logs", nil,
1222 arvados.ResourceListParams{
1223 Filters: []arvados.Filter{
1224 {Attr: "event_type", Operator: "=", Operand: "file_" + direction},
1225 {Attr: "object_uuid", Operator: "=", Operand: userUuid},
1228 Order: "created_at desc",
1230 c.Assert(err, check.IsNil)
1231 if len(logentries.Items) > 0 &&
1232 logentries.Items[0].ID > lastLogId &&
1233 logentries.Items[0].ObjectUUID == userUuid &&
1234 logentries.Items[0].Properties["collection_uuid"] == collectionUuid &&
1235 logentries.Items[0].Properties["collection_file_path"] == filepath {
1238 c.Logf("logentries.Items: %+v", logentries.Items)
1239 time.Sleep(50 * time.Millisecond)
1242 c.Check(resp.Result().StatusCode, check.Equals, http.StatusForbidden)
1243 c.Check(logbuf.String(), check.Equals, "")
1247 func (s *IntegrationSuite) TestDownloadLoggingPermission(c *check.C) {
1248 config := newConfig(ctxlog.TestLogger(c), s.ArvConfig)
1249 h := handler{Config: config}
1250 u := mustParseURL("http://" + arvadostest.FooCollection + ".keep-web.example/foo")
1252 config.cluster.Collections.TrustAllContent = true
1254 for _, adminperm := range []bool{true, false} {
1255 for _, userperm := range []bool{true, false} {
1256 config.cluster.Collections.WebDAVPermission.Admin.Download = adminperm
1257 config.cluster.Collections.WebDAVPermission.User.Download = userperm
1259 // Test admin permission
1260 req := &http.Request{
1264 RequestURI: u.RequestURI(),
1265 Header: http.Header{
1266 "Authorization": {"Bearer " + arvadostest.AdminToken},
1269 s.checkUploadDownloadRequest(c, &h, req, http.StatusOK, "download", adminperm,
1270 arvadostest.AdminUserUUID, arvadostest.FooCollection, "foo")
1272 // Test user permission
1273 req = &http.Request{
1277 RequestURI: u.RequestURI(),
1278 Header: http.Header{
1279 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1282 s.checkUploadDownloadRequest(c, &h, req, http.StatusOK, "download", userperm,
1283 arvadostest.ActiveUserUUID, arvadostest.FooCollection, "foo")
1287 config.cluster.Collections.WebDAVPermission.User.Download = true
1289 for _, tryurl := range []string{"http://" + arvadostest.MultilevelCollection1 + ".keep-web.example/dir1/subdir/file1",
1290 "http://keep-web/users/active/multilevel_collection_1/dir1/subdir/file1"} {
1292 u = mustParseURL(tryurl)
1293 req := &http.Request{
1297 RequestURI: u.RequestURI(),
1298 Header: http.Header{
1299 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1302 s.checkUploadDownloadRequest(c, &h, req, http.StatusOK, "download", true,
1303 arvadostest.ActiveUserUUID, arvadostest.MultilevelCollection1, "dir1/subdir/file1")
1306 u = mustParseURL("http://" + strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + ".keep-web.example/foo")
1307 req := &http.Request{
1311 RequestURI: u.RequestURI(),
1312 Header: http.Header{
1313 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1316 s.checkUploadDownloadRequest(c, &h, req, http.StatusOK, "download", true,
1317 arvadostest.ActiveUserUUID, arvadostest.FooCollection, "foo")
1320 func (s *IntegrationSuite) TestUploadLoggingPermission(c *check.C) {
1321 config := newConfig(ctxlog.TestLogger(c), s.ArvConfig)
1322 h := handler{Config: config}
1324 for _, adminperm := range []bool{true, false} {
1325 for _, userperm := range []bool{true, false} {
1327 arv := s.testServer.Config.Client
1328 arv.AuthToken = arvadostest.ActiveToken
1330 var coll arvados.Collection
1331 err := arv.RequestAndDecode(&coll,
1333 "/arvados/v1/collections",
1335 map[string]interface{}{
1336 "ensure_unique_name": true,
1337 "collection": map[string]interface{}{
1338 "name": "test collection",
1341 c.Assert(err, check.Equals, nil)
1343 u := mustParseURL("http://" + coll.UUID + ".keep-web.example/bar")
1345 config.cluster.Collections.WebDAVPermission.Admin.Upload = adminperm
1346 config.cluster.Collections.WebDAVPermission.User.Upload = userperm
1348 // Test admin permission
1349 req := &http.Request{
1353 RequestURI: u.RequestURI(),
1354 Header: http.Header{
1355 "Authorization": {"Bearer " + arvadostest.AdminToken},
1357 Body: io.NopCloser(bytes.NewReader([]byte("bar"))),
1359 s.checkUploadDownloadRequest(c, &h, req, http.StatusCreated, "upload", adminperm,
1360 arvadostest.AdminUserUUID, coll.UUID, "bar")
1362 // Test user permission
1363 req = &http.Request{
1367 RequestURI: u.RequestURI(),
1368 Header: http.Header{
1369 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1371 Body: io.NopCloser(bytes.NewReader([]byte("bar"))),
1373 s.checkUploadDownloadRequest(c, &h, req, http.StatusCreated, "upload", userperm,
1374 arvadostest.ActiveUserUUID, coll.UUID, "bar")