19889: Add www-authenticate header with 401 Unauthorized response.
authorTom Clegg <tom@curii.com>
Mon, 27 Mar 2023 23:24:02 +0000 (19:24 -0400)
committerTom Clegg <tom@curii.com>
Mon, 27 Mar 2023 23:24:02 +0000 (19:24 -0400)
Test webdav with cadaver.

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

lib/controller/localdb/container_gateway.go
lib/controller/localdb/container_gateway_test.go

index 8a70dc8e81264ceec663cf2d90d901b5140c4211..6cf787fcb4d857743f3b9513ab85b09695317885 100644 (file)
@@ -56,6 +56,13 @@ var (
 func (conn *Conn) ContainerLog(ctx context.Context, opts arvados.ContainerLogOptions) (http.Handler, error) {
        ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID, Select: []string{"uuid", "state", "gateway_address", "log"}})
        if err != nil {
+               if se := httpserver.HTTPStatusError(nil); errors.As(err, &se) && se.HTTPStatus() == http.StatusUnauthorized {
+                       // Hint to WebDAV client that we accept HTTP basic auth.
+                       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                               w.Header().Set("Www-Authenticate", "Basic realm=\"collections\"")
+                               w.WriteHeader(http.StatusUnauthorized)
+                       }), nil
+               }
                return nil, err
        }
        if ctr.GatewayAddress == "" ||
index 2a472199d5a1ecc51fa4b2535bb5b086b61bafe8..cc0011864e16939fc21605a763d561d71739bf7c 100644 (file)
@@ -5,6 +5,7 @@
 package localdb
 
 import (
+       "bytes"
        "crypto/hmac"
        "crypto/sha256"
        "fmt"
@@ -15,6 +16,8 @@ import (
        "net/http/httptest"
        "net/url"
        "os"
+       "os/exec"
+       "path/filepath"
        "strings"
        "time"
 
@@ -36,6 +39,7 @@ var _ = check.Suite(&ContainerGatewaySuite{})
 type ContainerGatewaySuite struct {
        localdbSuite
        ctrUUID string
+       srv     *httptest.Server
        gw      *crunchrun.Gateway
 }
 
@@ -49,15 +53,15 @@ func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
        authKey := fmt.Sprintf("%x", h.Sum(nil))
 
        rtr := router.New(s.localdb, router.Config{})
-       srv := httptest.NewUnstartedServer(rtr)
-       srv.StartTLS()
+       s.srv = httptest.NewUnstartedServer(rtr)
+       s.srv.StartTLS()
        // the test setup doesn't use lib/service so
        // service.URLFromContext() returns nothing -- instead, this
        // is how we advertise our internal URL and enable
        // proxy-to-other-controller mode,
-       forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: srv.Listener.Addr().String()}
+       forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: s.srv.Listener.Addr().String()}
        ac := &arvados.Client{
-               APIHost:   srv.Listener.Addr().String(),
+               APIHost:   s.srv.Listener.Addr().String(),
                AuthToken: arvadostest.Dispatch1Token,
                Insecure:  true,
        }
@@ -91,6 +95,11 @@ func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
        c.Check(err, check.IsNil)
 }
 
+func (s *ContainerGatewaySuite) TearDownTest(c *check.C) {
+       s.srv.Close()
+       s.localdbSuite.TearDownTest(c)
+}
+
 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
        for _, trial := range []struct {
                configAdmin bool
@@ -387,6 +396,57 @@ func (s *ContainerGatewaySuite) testContainerLog(c *check.C, viaGateway bool) {
        }
 }
 
+func (s *ContainerGatewaySuite) TestContainerLogViaCadaver(c *check.C) {
+       out := s.runCadaver(c, arvadostest.ActiveToken, "/arvados/v1/containers/"+s.ctrUUID+"/log", "ls")
+       c.Check(out, check.Matches, `(?ms).*stderr\.txt\s+12\s.*`)
+       c.Check(out, check.Matches, `(?ms).*a\s+0\s.*`)
+
+       out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/containers/"+s.ctrUUID+"/log", "get stderr.txt")
+       c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
+}
+
+func (s *ContainerGatewaySuite) runCadaver(c *check.C, password, path, stdin string) string {
+       // Replace s.srv with an HTTP server, otherwise cadaver will
+       // just fail on TLS cert verification.
+       s.srv.Close()
+       rtr := router.New(s.localdb, router.Config{})
+       s.srv = httptest.NewUnstartedServer(rtr)
+       s.srv.Start()
+
+       s.setupLogCollection(c, map[string]string{
+               "stderr.txt":   "hello world\n",
+               "a/b/c/d.html": "<html></html>\n",
+       })
+
+       tempdir, err := ioutil.TempDir("", "localdb-test-")
+       c.Assert(err, check.IsNil)
+       defer os.RemoveAll(tempdir)
+
+       cmd := exec.Command("cadaver", s.srv.URL+path)
+       if password != "" {
+               cmd.Env = append(os.Environ(), "HOME="+tempdir)
+               f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
+               c.Assert(err, check.IsNil)
+               _, err = fmt.Fprintf(f, "default login none password %s\n", password)
+               c.Assert(err, check.IsNil)
+               c.Assert(f.Close(), check.IsNil)
+       }
+       cmd.Stdin = bytes.NewBufferString(stdin)
+       cmd.Dir = tempdir
+       stdout, err := cmd.StdoutPipe()
+       c.Assert(err, check.Equals, nil)
+       cmd.Stderr = cmd.Stdout
+       c.Logf("cmd: %v", cmd.Args)
+       go cmd.Start()
+
+       var buf bytes.Buffer
+       _, err = io.Copy(&buf, stdout)
+       c.Check(err, check.Equals, nil)
+       err = cmd.Wait()
+       c.Check(err, check.Equals, nil)
+       return buf.String()
+}
+
 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
        c.Logf("connecting to %s", s.gw.Address)
        sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})