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/prometheus/client_golang/prometheus"
31 "github.com/sirupsen/logrus"
32 check "gopkg.in/check.v1"
35 var _ = check.Suite(&UnitSuite{})
38 arvados.DebugLocksPanicMode = true
41 type UnitSuite struct {
42 cluster *arvados.Cluster
46 func (s *UnitSuite) SetUpTest(c *check.C) {
47 logger := ctxlog.TestLogger(c)
48 ldr := config.NewLoader(bytes.NewBufferString("Clusters: {zzzzz: {}}"), logger)
50 cfg, err := ldr.Load()
51 c.Assert(err, check.IsNil)
52 cc, err := cfg.GetCluster("")
53 c.Assert(err, check.IsNil)
60 registry: prometheus.NewRegistry(),
65 func (s *UnitSuite) TestCORSPreflight(c *check.C) {
67 u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/foo")
72 RequestURI: u.RequestURI(),
74 "Origin": {"https://workbench.example"},
75 "Access-Control-Request-Method": {"POST"},
79 // Check preflight for an allowed request
80 resp := httptest.NewRecorder()
81 h.ServeHTTP(resp, req)
82 c.Check(resp.Code, check.Equals, http.StatusOK)
83 c.Check(resp.Body.String(), check.Equals, "")
84 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
85 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "COPY, DELETE, GET, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, RMCOL, UNLOCK")
86 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Authorization, Content-Type, Range, Depth, Destination, If, Lock-Token, Overwrite, Timeout, Cache-Control")
88 // Check preflight for a disallowed request
89 resp = httptest.NewRecorder()
90 req.Header.Set("Access-Control-Request-Method", "MAKE-COFFEE")
91 h.ServeHTTP(resp, req)
92 c.Check(resp.Body.String(), check.Equals, "")
93 c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
96 func (s *UnitSuite) TestWebdavPrefixAndSource(c *check.C) {
97 for _, trial := range []struct {
125 path: "/prefix/dir1/foo",
131 path: "/prefix/dir1/foo",
137 path: "/prefix/dir1/foo",
180 c.Logf("trial %+v", trial)
181 u := mustParseURL("http://" + arvadostest.FooBarDirCollection + ".keep-web.example" + trial.path)
182 req := &http.Request{
183 Method: trial.method,
186 RequestURI: u.RequestURI(),
188 "Authorization": {"Bearer " + arvadostest.ActiveTokenV2},
189 "X-Webdav-Prefix": {trial.prefix},
190 "X-Webdav-Source": {trial.source},
192 Body: ioutil.NopCloser(bytes.NewReader(nil)),
195 resp := httptest.NewRecorder()
196 s.handler.ServeHTTP(resp, req)
198 c.Check(resp.Code, check.Equals, http.StatusNotFound)
199 } else if trial.method == "PROPFIND" {
200 c.Check(resp.Code, check.Equals, http.StatusMultiStatus)
201 c.Check(resp.Body.String(), check.Matches, `(?ms).*>\n?$`)
202 } else if trial.seeOther {
203 c.Check(resp.Code, check.Equals, http.StatusSeeOther)
205 c.Check(resp.Code, check.Equals, http.StatusOK)
210 func (s *UnitSuite) TestEmptyResponse(c *check.C) {
211 for _, trial := range []struct {
217 // If we return no content due to a Keep read error,
218 // we should emit a log message.
219 {false, false, http.StatusOK, `(?ms).*only wrote 0 bytes.*`},
221 // If we return no content because the client sent an
222 // If-Modified-Since header, our response should be
223 // 304. We still expect a "File download" log since it
224 // counts as a file access for auditing.
225 {true, true, http.StatusNotModified, `(?ms).*msg="File download".*`},
227 c.Logf("trial: %+v", trial)
228 arvadostest.StartKeep(2, true)
229 if trial.dataExists {
230 arv, err := arvadosclient.MakeArvadosClient()
231 c.Assert(err, check.IsNil)
232 arv.ApiToken = arvadostest.ActiveToken
233 kc, err := keepclient.MakeKeepClient(arv)
234 c.Assert(err, check.IsNil)
235 _, _, err = kc.PutB([]byte("foo"))
236 c.Assert(err, check.IsNil)
239 u := mustParseURL("http://" + arvadostest.FooCollection + ".keep-web.example/foo")
240 req := &http.Request{
244 RequestURI: u.RequestURI(),
246 "Authorization": {"Bearer " + arvadostest.ActiveToken},
249 if trial.sendIMSHeader {
250 req.Header.Set("If-Modified-Since", strings.Replace(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT", -1))
253 var logbuf bytes.Buffer
254 logger := logrus.New()
256 req = req.WithContext(ctxlog.Context(context.Background(), logger))
258 resp := httptest.NewRecorder()
259 s.handler.ServeHTTP(resp, req)
260 c.Check(resp.Code, check.Equals, trial.expectStatus)
261 c.Check(resp.Body.String(), check.Equals, "")
263 c.Log(logbuf.String())
264 c.Check(logbuf.String(), check.Matches, trial.logRegexp)
268 func (s *UnitSuite) TestInvalidUUID(c *check.C) {
269 bogusID := strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + "-"
270 token := arvadostest.ActiveToken
271 for _, trial := range []string{
272 "http://keep-web/c=" + bogusID + "/foo",
273 "http://keep-web/c=" + bogusID + "/t=" + token + "/foo",
274 "http://keep-web/collections/download/" + bogusID + "/" + token + "/foo",
275 "http://keep-web/collections/" + bogusID + "/foo",
276 "http://" + bogusID + ".keep-web/" + bogusID + "/foo",
277 "http://" + bogusID + ".keep-web/t=" + token + "/" + bogusID + "/foo",
280 u := mustParseURL(trial)
281 req := &http.Request{
285 RequestURI: u.RequestURI(),
287 resp := httptest.NewRecorder()
288 s.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
289 s.handler.ServeHTTP(resp, req)
290 c.Check(resp.Code, check.Equals, http.StatusNotFound)
294 func mustParseURL(s string) *url.URL {
295 r, err := url.Parse(s)
297 panic("parse URL: " + s)
302 func (s *IntegrationSuite) TestVhost404(c *check.C) {
303 for _, testURL := range []string{
304 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
305 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
307 resp := httptest.NewRecorder()
308 u := mustParseURL(testURL)
309 req := &http.Request{
312 RequestURI: u.RequestURI(),
314 s.handler.ServeHTTP(resp, req)
315 c.Check(resp.Code, check.Equals, http.StatusNotFound)
316 c.Check(resp.Body.String(), check.Equals, notFoundMessage+"\n")
320 // An authorizer modifies an HTTP request to make use of the given
321 // token -- by adding it to a header, cookie, query param, or whatever
322 // -- and returns the HTTP status code we should expect from keep-web if
323 // the token is invalid.
324 type authorizer func(*http.Request, string) int
326 func (s *IntegrationSuite) TestVhostViaAuthzHeaderOAuth2(c *check.C) {
327 s.doVhostRequests(c, authzViaAuthzHeaderOAuth2)
329 func authzViaAuthzHeaderOAuth2(r *http.Request, tok string) int {
330 r.Header.Add("Authorization", "Bearer "+tok)
331 return http.StatusUnauthorized
333 func (s *IntegrationSuite) TestVhostViaAuthzHeaderBearer(c *check.C) {
334 s.doVhostRequests(c, authzViaAuthzHeaderBearer)
336 func authzViaAuthzHeaderBearer(r *http.Request, tok string) int {
337 r.Header.Add("Authorization", "Bearer "+tok)
338 return http.StatusUnauthorized
341 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
342 s.doVhostRequests(c, authzViaCookieValue)
344 func authzViaCookieValue(r *http.Request, tok string) int {
345 r.AddCookie(&http.Cookie{
346 Name: "arvados_api_token",
347 Value: auth.EncodeTokenCookie([]byte(tok)),
349 return http.StatusUnauthorized
352 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
353 s.doVhostRequests(c, authzViaPath)
355 func authzViaPath(r *http.Request, tok string) int {
356 r.URL.Path = "/t=" + tok + r.URL.Path
357 return http.StatusNotFound
360 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
361 s.doVhostRequests(c, authzViaQueryString)
363 func authzViaQueryString(r *http.Request, tok string) int {
364 r.URL.RawQuery = "api_token=" + tok
365 return http.StatusUnauthorized
368 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
369 s.doVhostRequests(c, authzViaPOST)
371 func authzViaPOST(r *http.Request, tok string) int {
373 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
374 r.Body = ioutil.NopCloser(strings.NewReader(
375 url.Values{"api_token": {tok}}.Encode()))
376 return http.StatusUnauthorized
379 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
380 s.doVhostRequests(c, authzViaPOST)
382 func authzViaXHRPOST(r *http.Request, tok string) int {
384 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
385 r.Header.Add("Origin", "https://origin.example")
386 r.Body = ioutil.NopCloser(strings.NewReader(
389 "disposition": {"attachment"},
391 return http.StatusUnauthorized
394 // Try some combinations of {url, token} using the given authorization
395 // mechanism, and verify the result is correct.
396 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
397 for _, hostPath := range []string{
398 arvadostest.FooCollection + ".example.com/foo",
399 arvadostest.FooCollection + "--collections.example.com/foo",
400 arvadostest.FooCollection + "--collections.example.com/_/foo",
401 arvadostest.FooCollectionPDH + ".example.com/foo",
402 strings.Replace(arvadostest.FooCollectionPDH, "+", "-", -1) + "--collections.example.com/foo",
403 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
405 c.Log("doRequests: ", hostPath)
406 s.doVhostRequestsWithHostPath(c, authz, hostPath)
410 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
411 for _, tok := range []string{
412 arvadostest.ActiveToken,
413 arvadostest.ActiveToken[:15],
414 arvadostest.SpectatorToken,
418 u := mustParseURL("http://" + hostPath)
419 req := &http.Request{
423 RequestURI: u.RequestURI(),
424 Header: http.Header{},
426 failCode := authz(req, tok)
427 req, resp := s.doReq(req)
428 code, body := resp.Code, resp.Body.String()
430 // If the initial request had a (non-empty) token
431 // showing in the query string, we should have been
432 // redirected in order to hide it in a cookie.
433 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
435 if tok == arvadostest.ActiveToken {
436 c.Check(code, check.Equals, http.StatusOK)
437 c.Check(body, check.Equals, "foo")
439 c.Check(code >= 400, check.Equals, true)
440 c.Check(code < 500, check.Equals, true)
441 if tok == arvadostest.SpectatorToken {
442 // Valid token never offers to retry
443 // with different credentials.
444 c.Check(code, check.Equals, http.StatusNotFound)
446 // Invalid token can ask to retry
447 // depending on the authz method.
448 c.Check(code, check.Equals, failCode)
451 c.Check(body, check.Equals, notFoundMessage+"\n")
453 c.Check(body, check.Equals, unauthorizedMessage+"\n")
459 func (s *IntegrationSuite) TestVhostPortMatch(c *check.C) {
460 for _, host := range []string{"download.example.com", "DOWNLOAD.EXAMPLE.COM"} {
461 for _, port := range []string{"80", "443", "8000"} {
462 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = fmt.Sprintf("download.example.com:%v", port)
463 u := mustParseURL(fmt.Sprintf("http://%v/by_id/%v/foo", host, arvadostest.FooCollection))
464 req := &http.Request{
468 RequestURI: u.RequestURI(),
469 Header: http.Header{"Authorization": []string{"Bearer " + arvadostest.ActiveToken}},
471 req, resp := s.doReq(req)
472 code, _ := resp.Code, resp.Body.String()
475 c.Check(code, check.Equals, 401)
477 c.Check(code, check.Equals, 200)
483 func (s *IntegrationSuite) do(method string, urlstring string, token string, hdr http.Header) (*http.Request, *httptest.ResponseRecorder) {
484 u := mustParseURL(urlstring)
485 if hdr == nil && token != "" {
486 hdr = http.Header{"Authorization": {"Bearer " + token}}
487 } else if hdr == nil {
489 } else if token != "" {
490 panic("must not pass both token and hdr")
492 return s.doReq(&http.Request{
496 RequestURI: u.RequestURI(),
501 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
502 resp := httptest.NewRecorder()
503 s.handler.ServeHTTP(resp, req)
504 if resp.Code != http.StatusSeeOther {
507 cookies := (&http.Response{Header: resp.Header()}).Cookies()
508 u, _ := req.URL.Parse(resp.Header().Get("Location"))
513 RequestURI: u.RequestURI(),
514 Header: http.Header{},
516 for _, c := range cookies {
522 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
523 s.testVhostRedirectTokenToCookie(c, "GET",
524 arvadostest.FooCollection+".example.com/foo",
525 "?api_token="+arvadostest.ActiveToken,
533 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
534 s.testVhostRedirectTokenToCookie(c, "GET",
535 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
544 func (s *IntegrationSuite) TestCollectionSharingToken(c *check.C) {
545 s.testVhostRedirectTokenToCookie(c, "GET",
546 "example.com/c="+arvadostest.FooFileCollectionUUID+"/t="+arvadostest.FooFileCollectionSharingToken+"/foo",
553 // Same valid sharing token, but requesting a different collection
554 s.testVhostRedirectTokenToCookie(c, "GET",
555 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.FooFileCollectionSharingToken+"/foo",
560 regexp.QuoteMeta(notFoundMessage+"\n"),
564 // Bad token in URL is 404 Not Found because it doesn't make sense to
565 // retry the same URL with different authorization.
566 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
567 s.testVhostRedirectTokenToCookie(c, "GET",
568 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
573 regexp.QuoteMeta(notFoundMessage+"\n"),
577 // Bad token in a cookie (even if it got there via our own
578 // query-string-to-cookie redirect) is, in principle, retryable via
579 // wb2-login-and-redirect flow.
580 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
582 resp := s.testVhostRedirectTokenToCookie(c, "GET",
583 arvadostest.FooCollection+".example.com/foo",
584 "?api_token=thisisabogustoken",
585 http.Header{"Sec-Fetch-Mode": {"navigate"}},
590 u, err := url.Parse(resp.Header().Get("Location"))
591 c.Assert(err, check.IsNil)
592 c.Logf("redirected to %s", u)
593 c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
594 c.Check(u.Query().Get("redirectToPreview"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
595 c.Check(u.Query().Get("redirectToDownload"), check.Equals, "")
597 // Download/attachment indicated by ?disposition=attachment
598 resp = s.testVhostRedirectTokenToCookie(c, "GET",
599 arvadostest.FooCollection+".example.com/foo",
600 "?api_token=thisisabogustoken&disposition=attachment",
601 http.Header{"Sec-Fetch-Mode": {"navigate"}},
606 u, err = url.Parse(resp.Header().Get("Location"))
607 c.Assert(err, check.IsNil)
608 c.Logf("redirected to %s", u)
609 c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
610 c.Check(u.Query().Get("redirectToPreview"), check.Equals, "")
611 c.Check(u.Query().Get("redirectToDownload"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
613 // Download/attachment indicated by vhost
614 resp = s.testVhostRedirectTokenToCookie(c, "GET",
615 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
616 "?api_token=thisisabogustoken",
617 http.Header{"Sec-Fetch-Mode": {"navigate"}},
622 u, err = url.Parse(resp.Header().Get("Location"))
623 c.Assert(err, check.IsNil)
624 c.Logf("redirected to %s", u)
625 c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
626 c.Check(u.Query().Get("redirectToPreview"), check.Equals, "")
627 c.Check(u.Query().Get("redirectToDownload"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
629 // Without "Sec-Fetch-Mode: navigate" header, just 401.
630 s.testVhostRedirectTokenToCookie(c, "GET",
631 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
632 "?api_token=thisisabogustoken",
633 http.Header{"Sec-Fetch-Mode": {"cors"}},
635 http.StatusUnauthorized,
636 regexp.QuoteMeta(unauthorizedMessage+"\n"),
638 s.testVhostRedirectTokenToCookie(c, "GET",
639 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
640 "?api_token=thisisabogustoken",
643 http.StatusUnauthorized,
644 regexp.QuoteMeta(unauthorizedMessage+"\n"),
648 func (s *IntegrationSuite) TestVhostRedirectWithNoCache(c *check.C) {
649 resp := s.testVhostRedirectTokenToCookie(c, "GET",
650 arvadostest.FooCollection+".example.com/foo",
651 "?api_token=thisisabogustoken",
653 "Sec-Fetch-Mode": {"navigate"},
654 "Cache-Control": {"no-cache"},
660 u, err := url.Parse(resp.Header().Get("Location"))
661 c.Assert(err, check.IsNil)
662 c.Logf("redirected to %s", u)
663 c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
664 c.Check(u.Query().Get("redirectToPreview"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
665 c.Check(u.Query().Get("redirectToDownload"), check.Equals, "")
668 func (s *IntegrationSuite) TestNoTokenWorkbench2LoginFlow(c *check.C) {
669 for _, trial := range []struct {
674 {cacheControl: "no-cache"},
676 {anonToken: true, cacheControl: "no-cache"},
678 c.Logf("trial: %+v", trial)
681 s.handler.Cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
683 s.handler.Cluster.Users.AnonymousUserToken = ""
685 req, err := http.NewRequest("GET", "http://"+arvadostest.FooCollection+".example.com/foo", nil)
686 c.Assert(err, check.IsNil)
687 req.Header.Set("Sec-Fetch-Mode", "navigate")
688 if trial.cacheControl != "" {
689 req.Header.Set("Cache-Control", trial.cacheControl)
691 resp := httptest.NewRecorder()
692 s.handler.ServeHTTP(resp, req)
693 c.Check(resp.Code, check.Equals, http.StatusSeeOther)
694 u, err := url.Parse(resp.Header().Get("Location"))
695 c.Assert(err, check.IsNil)
696 c.Logf("redirected to %q", u)
697 c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
698 c.Check(u.Query().Get("redirectToPreview"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
699 c.Check(u.Query().Get("redirectToDownload"), check.Equals, "")
703 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
704 s.testVhostRedirectTokenToCookie(c, "GET",
705 "example.com/c="+arvadostest.FooCollection+"/foo",
706 "?api_token="+arvadostest.ActiveToken,
709 http.StatusBadRequest,
710 regexp.QuoteMeta("cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n"),
714 // If client requests an attachment by putting ?disposition=attachment
715 // in the query string, and gets redirected, the redirect target
716 // should respond with an attachment.
717 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
718 resp := s.testVhostRedirectTokenToCookie(c, "GET",
719 arvadostest.FooCollection+".example.com/foo",
720 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
726 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
729 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
730 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
731 resp := s.testVhostRedirectTokenToCookie(c, "GET",
732 "download.example.com/by_id/"+arvadostest.FooCollection+"/foo",
733 "?api_token="+arvadostest.ActiveToken,
739 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
742 func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
743 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
744 resp := s.testVhostRedirectTokenToCookie(c, "GET",
745 "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
746 "?api_token="+arvadostest.ActiveToken,
752 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
753 resp = s.testVhostRedirectTokenToCookie(c, "GET",
754 "download.example.com/by_id/"+arvadostest.WazVersion1Collection+"/waz",
755 "?api_token="+arvadostest.ActiveToken,
761 c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
764 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
765 s.handler.Cluster.Collections.TrustAllContent = true
766 s.testVhostRedirectTokenToCookie(c, "GET",
767 "example.com/c="+arvadostest.FooCollection+"/foo",
768 "?api_token="+arvadostest.ActiveToken,
776 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
777 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com:1234"
779 s.testVhostRedirectTokenToCookie(c, "GET",
780 "example.com/c="+arvadostest.FooCollection+"/foo",
781 "?api_token="+arvadostest.ActiveToken,
784 http.StatusBadRequest,
785 regexp.QuoteMeta("cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n"),
788 resp := s.testVhostRedirectTokenToCookie(c, "GET",
789 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
790 "?api_token="+arvadostest.ActiveToken,
796 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
799 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
800 s.testVhostRedirectTokenToCookie(c, "POST",
801 arvadostest.FooCollection+".example.com/foo",
803 http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
804 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
810 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
811 s.testVhostRedirectTokenToCookie(c, "POST",
812 arvadostest.FooCollection+".example.com/foo",
814 http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
815 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
817 regexp.QuoteMeta(notFoundMessage+"\n"),
821 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
822 s.handler.Cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
823 s.testVhostRedirectTokenToCookie(c, "GET",
824 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
833 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
834 s.handler.Cluster.Users.AnonymousUserToken = "anonymousTokenConfiguredButInvalid"
835 s.testVhostRedirectTokenToCookie(c, "GET",
836 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
840 http.StatusUnauthorized,
841 "Authorization tokens are not accepted here: .*\n",
845 func (s *IntegrationSuite) TestSpecialCharsInPath(c *check.C) {
846 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
848 client := arvados.NewClientFromEnv()
849 client.AuthToken = arvadostest.ActiveToken
850 fs, err := (&arvados.Collection{}).FileSystem(client, nil)
851 c.Assert(err, check.IsNil)
852 f, err := fs.OpenFile("https:\\\"odd' path chars", os.O_CREATE, 0777)
853 c.Assert(err, check.IsNil)
855 mtxt, err := fs.MarshalManifest(".")
856 c.Assert(err, check.IsNil)
857 var coll arvados.Collection
858 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
859 "collection": map[string]string{
860 "manifest_text": mtxt,
863 c.Assert(err, check.IsNil)
865 u, _ := url.Parse("http://download.example.com/c=" + coll.UUID + "/")
866 req := &http.Request{
870 RequestURI: u.RequestURI(),
872 "Authorization": {"Bearer " + client.AuthToken},
875 resp := httptest.NewRecorder()
876 s.handler.ServeHTTP(resp, req)
877 c.Check(resp.Code, check.Equals, http.StatusOK)
878 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./https:%5c%22odd%27%20path%20chars"\S+https:\\"odd' path chars.*`)
881 func (s *IntegrationSuite) TestForwardSlashSubstitution(c *check.C) {
882 arv := arvados.NewClientFromEnv()
883 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
884 s.handler.Cluster.Collections.ForwardSlashNameSubstitution = "{SOLIDUS}"
885 name := "foo/bar/baz"
886 nameShown := strings.Replace(name, "/", "{SOLIDUS}", -1)
887 nameShownEscaped := strings.Replace(name, "/", "%7bSOLIDUS%7d", -1)
889 client := arvados.NewClientFromEnv()
890 client.AuthToken = arvadostest.ActiveToken
891 fs, err := (&arvados.Collection{}).FileSystem(client, nil)
892 c.Assert(err, check.IsNil)
893 f, err := fs.OpenFile("filename", os.O_CREATE, 0777)
894 c.Assert(err, check.IsNil)
896 mtxt, err := fs.MarshalManifest(".")
897 c.Assert(err, check.IsNil)
898 var coll arvados.Collection
899 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
900 "collection": map[string]string{
901 "manifest_text": mtxt,
903 "owner_uuid": arvadostest.AProjectUUID,
906 c.Assert(err, check.IsNil)
907 defer arv.RequestAndDecode(&coll, "DELETE", "arvados/v1/collections/"+coll.UUID, nil, nil)
909 base := "http://download.example.com/by_id/" + coll.OwnerUUID + "/"
910 for tryURL, expectRegexp := range map[string]string{
911 base: `(?ms).*href="./` + nameShownEscaped + `/"\S+` + nameShown + `.*`,
912 base + nameShownEscaped + "/": `(?ms).*href="./filename"\S+filename.*`,
914 u, _ := url.Parse(tryURL)
915 req := &http.Request{
919 RequestURI: u.RequestURI(),
921 "Authorization": {"Bearer " + client.AuthToken},
924 resp := httptest.NewRecorder()
925 s.handler.ServeHTTP(resp, req)
926 c.Check(resp.Code, check.Equals, http.StatusOK)
927 c.Check(resp.Body.String(), check.Matches, expectRegexp)
931 // XHRs can't follow redirect-with-cookie so they rely on method=POST
932 // and disposition=attachment (telling us it's acceptable to respond
933 // with content instead of a redirect) and an Origin header that gets
934 // added automatically by the browser (telling us it's desirable to do
936 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
937 u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
938 req := &http.Request{
942 RequestURI: u.RequestURI(),
944 "Origin": {"https://origin.example"},
945 "Content-Type": {"application/x-www-form-urlencoded"},
947 Body: ioutil.NopCloser(strings.NewReader(url.Values{
948 "api_token": {arvadostest.ActiveToken},
949 "disposition": {"attachment"},
952 resp := httptest.NewRecorder()
953 s.handler.ServeHTTP(resp, req)
954 c.Check(resp.Code, check.Equals, http.StatusOK)
955 c.Check(resp.Body.String(), check.Equals, "foo")
956 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
958 // GET + Origin header is representative of both AJAX GET
959 // requests and inline images via <IMG crossorigin="anonymous"
961 u.RawQuery = "api_token=" + url.QueryEscape(arvadostest.ActiveTokenV2)
966 RequestURI: u.RequestURI(),
968 "Origin": {"https://origin.example"},
971 resp = httptest.NewRecorder()
972 s.handler.ServeHTTP(resp, req)
973 c.Check(resp.Code, check.Equals, http.StatusOK)
974 c.Check(resp.Body.String(), check.Equals, "foo")
975 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
978 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString string, reqHeader http.Header, reqBody string, expectStatus int, matchRespBody string) *httptest.ResponseRecorder {
979 if reqHeader == nil {
980 reqHeader = http.Header{}
982 u, _ := url.Parse(`http://` + hostPath + queryString)
983 c.Logf("requesting %s", u)
984 req := &http.Request{
988 RequestURI: u.RequestURI(),
990 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
993 resp := httptest.NewRecorder()
995 c.Check(resp.Code, check.Equals, expectStatus)
996 c.Check(resp.Body.String(), check.Matches, matchRespBody)
999 s.handler.ServeHTTP(resp, req)
1000 if resp.Code != http.StatusSeeOther {
1003 c.Check(resp.Body.String(), check.Matches, `.*href="http://`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
1004 c.Check(strings.Split(resp.Header().Get("Location"), "?")[0], check.Equals, "http://"+hostPath)
1005 cookies := (&http.Response{Header: resp.Header()}).Cookies()
1007 u, err := u.Parse(resp.Header().Get("Location"))
1008 c.Assert(err, check.IsNil)
1009 c.Logf("following redirect to %s", u)
1010 req = &http.Request{
1014 RequestURI: u.RequestURI(),
1017 for _, c := range cookies {
1021 resp = httptest.NewRecorder()
1022 s.handler.ServeHTTP(resp, req)
1024 if resp.Code != http.StatusSeeOther {
1025 c.Check(resp.Header().Get("Location"), check.Equals, "")
1030 func (s *IntegrationSuite) TestDirectoryListingWithAnonymousToken(c *check.C) {
1031 s.handler.Cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
1032 s.testDirectoryListing(c)
1035 func (s *IntegrationSuite) TestDirectoryListingWithNoAnonymousToken(c *check.C) {
1036 s.handler.Cluster.Users.AnonymousUserToken = ""
1037 s.testDirectoryListing(c)
1040 func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
1041 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
1042 authHeader := http.Header{
1043 "Authorization": {"OAuth2 " + arvadostest.ActiveToken},
1045 for _, trial := range []struct {
1053 uri: strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/",
1055 expect: []string{"dir1/foo", "dir1/bar"},
1059 uri: strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/dir1/",
1061 expect: []string{"foo", "bar"},
1065 // URLs of this form ignore authHeader, and
1066 // FooAndBarFilesInDirUUID isn't public, so
1067 // this returns 401.
1068 uri: "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
1073 uri: "download.example.com/users/active/foo_file_in_dir/",
1075 expect: []string{"dir1/"},
1079 uri: "download.example.com/users/active/foo_file_in_dir/dir1/",
1081 expect: []string{"bar"},
1085 uri: "download.example.com/",
1087 expect: []string{"users/"},
1091 uri: "download.example.com/users",
1093 redirect: "/users/",
1094 expect: []string{"active/"},
1098 uri: "download.example.com/users/",
1100 expect: []string{"active/"},
1104 uri: "download.example.com/users/active",
1106 redirect: "/users/active/",
1107 expect: []string{"foo_file_in_dir/"},
1111 uri: "download.example.com/users/active/",
1113 expect: []string{"foo_file_in_dir/"},
1117 uri: "collections.example.com/collections/download/" + arvadostest.FooAndBarFilesInDirUUID + "/" + arvadostest.ActiveToken + "/",
1119 expect: []string{"dir1/foo", "dir1/bar"},
1123 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/",
1125 expect: []string{"dir1/foo", "dir1/bar"},
1129 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken,
1131 expect: []string{"dir1/foo", "dir1/bar"},
1135 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID,
1137 expect: []string{"dir1/foo", "dir1/bar"},
1141 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1",
1143 redirect: "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1/",
1144 expect: []string{"foo", "bar"},
1148 uri: "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/_/dir1/",
1150 expect: []string{"foo", "bar"},
1154 uri: arvadostest.FooAndBarFilesInDirUUID + ".example.com/dir1?api_token=" + arvadostest.ActiveToken,
1157 expect: []string{"foo", "bar"},
1161 uri: "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/theperthcountyconspiracydoesnotexist/",
1166 uri: "download.example.com/c=" + arvadostest.WazVersion1Collection,
1168 expect: []string{"waz"},
1172 uri: "download.example.com/by_id/" + arvadostest.WazVersion1Collection,
1174 expect: []string{"waz"},
1178 comment := check.Commentf("HTML: %q => %q", trial.uri, trial.expect)
1179 resp := httptest.NewRecorder()
1180 u := mustParseURL("//" + trial.uri)
1181 req := &http.Request{
1185 RequestURI: u.RequestURI(),
1186 Header: copyHeader(trial.header),
1188 s.handler.ServeHTTP(resp, req)
1189 var cookies []*http.Cookie
1190 for resp.Code == http.StatusSeeOther {
1191 u, _ := req.URL.Parse(resp.Header().Get("Location"))
1192 req = &http.Request{
1196 RequestURI: u.RequestURI(),
1197 Header: copyHeader(trial.header),
1199 cookies = append(cookies, (&http.Response{Header: resp.Header()}).Cookies()...)
1200 for _, c := range cookies {
1203 resp = httptest.NewRecorder()
1204 s.handler.ServeHTTP(resp, req)
1206 if trial.redirect != "" {
1207 c.Check(req.URL.Path, check.Equals, trial.redirect, comment)
1209 if trial.expect == nil {
1210 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
1212 c.Check(resp.Code, check.Equals, http.StatusOK, comment)
1213 for _, e := range trial.expect {
1214 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./`+e+`".*`, comment)
1216 c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`, comment)
1219 comment = check.Commentf("WebDAV: %q => %q", trial.uri, trial.expect)
1220 req = &http.Request{
1224 RequestURI: u.RequestURI(),
1225 Header: copyHeader(trial.header),
1226 Body: ioutil.NopCloser(&bytes.Buffer{}),
1228 resp = httptest.NewRecorder()
1229 s.handler.ServeHTTP(resp, req)
1230 if trial.expect == nil {
1231 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
1233 c.Check(resp.Code, check.Equals, http.StatusOK, comment)
1236 req = &http.Request{
1240 RequestURI: u.RequestURI(),
1241 Header: copyHeader(trial.header),
1242 Body: ioutil.NopCloser(&bytes.Buffer{}),
1244 resp = httptest.NewRecorder()
1245 s.handler.ServeHTTP(resp, req)
1246 if trial.expect == nil {
1247 c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
1249 c.Check(resp.Code, check.Equals, http.StatusMultiStatus, comment)
1250 for _, e := range trial.expect {
1251 if strings.HasSuffix(e, "/") {
1252 e = filepath.Join(u.Path, e) + "/"
1254 e = filepath.Join(u.Path, e)
1256 c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+e+`</D:href>.*`, comment)
1262 func (s *IntegrationSuite) TestDeleteLastFile(c *check.C) {
1263 arv := arvados.NewClientFromEnv()
1264 var newCollection arvados.Collection
1265 err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
1266 "collection": map[string]string{
1267 "owner_uuid": arvadostest.ActiveUserUUID,
1268 "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
1269 "name": "keep-web test collection",
1271 "ensure_unique_name": true,
1273 c.Assert(err, check.IsNil)
1274 defer arv.RequestAndDecode(&newCollection, "DELETE", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
1276 var updated arvados.Collection
1277 for _, fnm := range []string{"foo.txt", "bar.txt"} {
1278 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com"
1279 u, _ := url.Parse("http://example.com/c=" + newCollection.UUID + "/" + fnm)
1280 req := &http.Request{
1284 RequestURI: u.RequestURI(),
1285 Header: http.Header{
1286 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1289 resp := httptest.NewRecorder()
1290 s.handler.ServeHTTP(resp, req)
1291 c.Check(resp.Code, check.Equals, http.StatusNoContent)
1293 updated = arvados.Collection{}
1294 err = arv.RequestAndDecode(&updated, "GET", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
1295 c.Check(err, check.IsNil)
1296 c.Check(updated.ManifestText, check.Not(check.Matches), `(?ms).*\Q`+fnm+`\E.*`)
1297 c.Logf("updated manifest_text %q", updated.ManifestText)
1299 c.Check(updated.ManifestText, check.Equals, "")
1302 func (s *IntegrationSuite) TestFileContentType(c *check.C) {
1303 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
1305 client := arvados.NewClientFromEnv()
1306 client.AuthToken = arvadostest.ActiveToken
1307 arv, err := arvadosclient.New(client)
1308 c.Assert(err, check.Equals, nil)
1309 kc, err := keepclient.MakeKeepClient(arv)
1310 c.Assert(err, check.Equals, nil)
1312 fs, err := (&arvados.Collection{}).FileSystem(client, kc)
1313 c.Assert(err, check.IsNil)
1315 trials := []struct {
1320 {"picture.txt", "BMX bikes are small this year\n", "text/plain; charset=utf-8"},
1321 {"picture.bmp", "BMX bikes are small this year\n", "image/(x-ms-)?bmp"},
1322 {"picture.jpg", "BMX bikes are small this year\n", "image/jpeg"},
1323 {"picture1", "BMX bikes are small this year\n", "image/bmp"}, // content sniff; "BM" is the magic signature for .bmp
1324 {"picture2", "Cars are small this year\n", "text/plain; charset=utf-8"}, // content sniff
1326 for _, trial := range trials {
1327 f, err := fs.OpenFile(trial.filename, os.O_CREATE|os.O_WRONLY, 0777)
1328 c.Assert(err, check.IsNil)
1329 _, err = f.Write([]byte(trial.content))
1330 c.Assert(err, check.IsNil)
1331 c.Assert(f.Close(), check.IsNil)
1333 mtxt, err := fs.MarshalManifest(".")
1334 c.Assert(err, check.IsNil)
1335 var coll arvados.Collection
1336 err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
1337 "collection": map[string]string{
1338 "manifest_text": mtxt,
1341 c.Assert(err, check.IsNil)
1343 for _, trial := range trials {
1344 u, _ := url.Parse("http://download.example.com/by_id/" + coll.UUID + "/" + trial.filename)
1345 req := &http.Request{
1349 RequestURI: u.RequestURI(),
1350 Header: http.Header{
1351 "Authorization": {"Bearer " + client.AuthToken},
1354 resp := httptest.NewRecorder()
1355 s.handler.ServeHTTP(resp, req)
1356 c.Check(resp.Code, check.Equals, http.StatusOK)
1357 c.Check(resp.Header().Get("Content-Type"), check.Matches, trial.contentType)
1358 c.Check(resp.Body.String(), check.Equals, trial.content)
1362 func (s *IntegrationSuite) TestKeepClientBlockCache(c *check.C) {
1363 s.handler.Cluster.Collections.WebDAVCache.MaxBlockEntries = 42
1364 c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Not(check.Equals), 42)
1365 u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/t=" + arvadostest.ActiveToken + "/foo")
1366 req := &http.Request{
1370 RequestURI: u.RequestURI(),
1372 resp := httptest.NewRecorder()
1373 s.handler.ServeHTTP(resp, req)
1374 c.Check(resp.Code, check.Equals, http.StatusOK)
1375 c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Equals, 42)
1378 // Writing to a collection shouldn't affect its entry in the
1379 // PDH-to-manifest cache.
1380 func (s *IntegrationSuite) TestCacheWriteCollectionSamePDH(c *check.C) {
1381 arv, err := arvadosclient.MakeArvadosClient()
1382 c.Assert(err, check.Equals, nil)
1383 arv.ApiToken = arvadostest.ActiveToken
1385 u := mustParseURL("http://x.example/testfile")
1386 req := &http.Request{
1390 RequestURI: u.RequestURI(),
1391 Header: http.Header{"Authorization": {"Bearer " + arv.ApiToken}},
1394 checkWithID := func(id string, status int) {
1395 req.URL.Host = strings.Replace(id, "+", "-", -1) + ".example"
1396 req.Host = req.URL.Host
1397 resp := httptest.NewRecorder()
1398 s.handler.ServeHTTP(resp, req)
1399 c.Check(resp.Code, check.Equals, status)
1402 var colls [2]arvados.Collection
1403 for i := range colls {
1404 err := arv.Create("collections",
1405 map[string]interface{}{
1406 "ensure_unique_name": true,
1407 "collection": map[string]interface{}{
1408 "name": "test collection",
1411 c.Assert(err, check.Equals, nil)
1414 // Populate cache with empty collection
1415 checkWithID(colls[0].PortableDataHash, http.StatusNotFound)
1417 // write a file to colls[0]
1419 reqPut.Method = "PUT"
1420 reqPut.URL.Host = colls[0].UUID + ".example"
1421 reqPut.Host = req.URL.Host
1422 reqPut.Body = ioutil.NopCloser(bytes.NewBufferString("testdata"))
1423 resp := httptest.NewRecorder()
1424 s.handler.ServeHTTP(resp, &reqPut)
1425 c.Check(resp.Code, check.Equals, http.StatusCreated)
1427 // new file should not appear in colls[1]
1428 checkWithID(colls[1].PortableDataHash, http.StatusNotFound)
1429 checkWithID(colls[1].UUID, http.StatusNotFound)
1431 checkWithID(colls[0].UUID, http.StatusOK)
1434 func copyHeader(h http.Header) http.Header {
1436 for k, v := range h {
1437 hc[k] = append([]string(nil), v...)
1442 func (s *IntegrationSuite) checkUploadDownloadRequest(c *check.C, req *http.Request,
1443 successCode int, direction string, perm bool, userUuid, collectionUuid, collectionPDH, filepath string) {
1445 client := arvados.NewClientFromEnv()
1446 client.AuthToken = arvadostest.AdminToken
1447 var logentries arvados.LogList
1449 err := client.RequestAndDecode(&logentries, "GET", "arvados/v1/logs", nil,
1450 arvados.ResourceListParams{
1452 Order: "created_at desc"})
1453 c.Check(err, check.IsNil)
1454 c.Check(logentries.Items, check.HasLen, 1)
1455 lastLogId := logentries.Items[0].ID
1456 c.Logf("lastLogId: %d", lastLogId)
1458 var logbuf bytes.Buffer
1459 logger := logrus.New()
1460 logger.Out = &logbuf
1461 resp := httptest.NewRecorder()
1462 req = req.WithContext(ctxlog.Context(context.Background(), logger))
1463 s.handler.ServeHTTP(resp, req)
1466 c.Check(resp.Result().StatusCode, check.Equals, successCode)
1467 c.Check(logbuf.String(), check.Matches, `(?ms).*msg="File `+direction+`".*`)
1468 c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*level=error.*`)
1470 deadline := time.Now().Add(time.Second)
1472 c.Assert(time.Now().After(deadline), check.Equals, false, check.Commentf("timed out waiting for log entry"))
1473 logentries = arvados.LogList{}
1474 err = client.RequestAndDecode(&logentries, "GET", "arvados/v1/logs", nil,
1475 arvados.ResourceListParams{
1476 Filters: []arvados.Filter{
1477 {Attr: "event_type", Operator: "=", Operand: "file_" + direction},
1478 {Attr: "object_uuid", Operator: "=", Operand: userUuid},
1481 Order: "created_at desc",
1483 c.Assert(err, check.IsNil)
1484 if len(logentries.Items) > 0 &&
1485 logentries.Items[0].ID > lastLogId &&
1486 logentries.Items[0].ObjectUUID == userUuid &&
1487 logentries.Items[0].Properties["collection_uuid"] == collectionUuid &&
1488 (collectionPDH == "" || logentries.Items[0].Properties["portable_data_hash"] == collectionPDH) &&
1489 logentries.Items[0].Properties["collection_file_path"] == filepath {
1492 c.Logf("logentries.Items: %+v", logentries.Items)
1493 time.Sleep(50 * time.Millisecond)
1496 c.Check(resp.Result().StatusCode, check.Equals, http.StatusForbidden)
1497 c.Check(logbuf.String(), check.Equals, "")
1501 func (s *IntegrationSuite) TestDownloadLoggingPermission(c *check.C) {
1502 u := mustParseURL("http://" + arvadostest.FooCollection + ".keep-web.example/foo")
1504 s.handler.Cluster.Collections.TrustAllContent = true
1506 for _, adminperm := range []bool{true, false} {
1507 for _, userperm := range []bool{true, false} {
1508 s.handler.Cluster.Collections.WebDAVPermission.Admin.Download = adminperm
1509 s.handler.Cluster.Collections.WebDAVPermission.User.Download = userperm
1511 // Test admin permission
1512 req := &http.Request{
1516 RequestURI: u.RequestURI(),
1517 Header: http.Header{
1518 "Authorization": {"Bearer " + arvadostest.AdminToken},
1521 s.checkUploadDownloadRequest(c, req, http.StatusOK, "download", adminperm,
1522 arvadostest.AdminUserUUID, arvadostest.FooCollection, arvadostest.FooCollectionPDH, "foo")
1524 // Test user permission
1525 req = &http.Request{
1529 RequestURI: u.RequestURI(),
1530 Header: http.Header{
1531 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1534 s.checkUploadDownloadRequest(c, req, http.StatusOK, "download", userperm,
1535 arvadostest.ActiveUserUUID, arvadostest.FooCollection, arvadostest.FooCollectionPDH, "foo")
1539 s.handler.Cluster.Collections.WebDAVPermission.User.Download = true
1541 for _, tryurl := range []string{"http://" + arvadostest.MultilevelCollection1 + ".keep-web.example/dir1/subdir/file1",
1542 "http://keep-web/users/active/multilevel_collection_1/dir1/subdir/file1"} {
1544 u = mustParseURL(tryurl)
1545 req := &http.Request{
1549 RequestURI: u.RequestURI(),
1550 Header: http.Header{
1551 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1554 s.checkUploadDownloadRequest(c, req, http.StatusOK, "download", true,
1555 arvadostest.ActiveUserUUID, arvadostest.MultilevelCollection1, arvadostest.MultilevelCollection1PDH, "dir1/subdir/file1")
1558 u = mustParseURL("http://" + strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + ".keep-web.example/foo")
1559 req := &http.Request{
1563 RequestURI: u.RequestURI(),
1564 Header: http.Header{
1565 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1568 s.checkUploadDownloadRequest(c, req, http.StatusOK, "download", true,
1569 arvadostest.ActiveUserUUID, "", arvadostest.FooCollectionPDH, "foo")
1572 func (s *IntegrationSuite) TestUploadLoggingPermission(c *check.C) {
1573 for _, adminperm := range []bool{true, false} {
1574 for _, userperm := range []bool{true, false} {
1576 arv := arvados.NewClientFromEnv()
1577 arv.AuthToken = arvadostest.ActiveToken
1579 var coll arvados.Collection
1580 err := arv.RequestAndDecode(&coll,
1582 "/arvados/v1/collections",
1584 map[string]interface{}{
1585 "ensure_unique_name": true,
1586 "collection": map[string]interface{}{
1587 "name": "test collection",
1590 c.Assert(err, check.Equals, nil)
1592 u := mustParseURL("http://" + coll.UUID + ".keep-web.example/bar")
1594 s.handler.Cluster.Collections.WebDAVPermission.Admin.Upload = adminperm
1595 s.handler.Cluster.Collections.WebDAVPermission.User.Upload = userperm
1597 // Test admin permission
1598 req := &http.Request{
1602 RequestURI: u.RequestURI(),
1603 Header: http.Header{
1604 "Authorization": {"Bearer " + arvadostest.AdminToken},
1606 Body: io.NopCloser(bytes.NewReader([]byte("bar"))),
1608 s.checkUploadDownloadRequest(c, req, http.StatusCreated, "upload", adminperm,
1609 arvadostest.AdminUserUUID, coll.UUID, "", "bar")
1611 // Test user permission
1612 req = &http.Request{
1616 RequestURI: u.RequestURI(),
1617 Header: http.Header{
1618 "Authorization": {"Bearer " + arvadostest.ActiveToken},
1620 Body: io.NopCloser(bytes.NewReader([]byte("bar"))),
1622 s.checkUploadDownloadRequest(c, req, http.StatusCreated, "upload", userperm,
1623 arvadostest.ActiveUserUUID, coll.UUID, "", "bar")