Merge branch 'master' into 14716-webdav-cluster-config
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 6 Aug 2019 19:36:27 +0000 (16:36 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 6 Aug 2019 19:36:27 +0000 (16:36 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

1  2 
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/export.go
lib/config/generated_config.go
sdk/go/arvados/config.go
services/keep-web/handler.go
services/keep-web/handler_test.go
services/keep-web/server_test.go

index 39b5e41cc5c2f1f2d3d1c3688ba7dbf48508c858,9ac4aeeb9606461dd23f0f743e6a17a065662c76..7fd6306185ed7930d2e64cc1d9387a1f16b19f95
@@@ -212,8 -212,8 +212,8 @@@ Clusters
        # to run an open instance where anyone can create an account and use
        # the system without requiring manual approval.
        #
 -      # The params auto_setup_new_users_with_* are meaningful only when auto_setup_new_users is turned on.
 -      # auto_setup_name_blacklist is a list of usernames to be blacklisted for auto setup.
 +      # The params AutoSetupNewUsersWith* are meaningful only when AutoSetupNewUsers is turned on.
 +      # AutoSetupUsernameBlacklist is a list of usernames to be blacklisted for auto setup.
        AutoSetupNewUsers: false
        AutoSetupNewUsersWithVmUUID: ""
        AutoSetupNewUsersWithRepository: false
          syslog: {}
          SAMPLE: {}
  
 -      # When new_users_are_active is set to true, new users will be active
 +      # When NewUsersAreActive is set to true, new users will be active
        # immediately.  This skips the "self-activate" step which enforces
        # user agreements.  Should only be enabled for development.
        NewUsersAreActive: false
        # should be an address associated with a Google account.
        AutoAdminUserWithEmail: ""
  
 -      # If auto_admin_first_user is set to true, the first user to log in when no
 +      # If AutoAdminFirstUser is set to true, the first user to log in when no
        # other admin users exist will automatically become an admin user.
        AutoAdminFirstUser: false
  
        # in the directory where your API server is running.
        AnonymousUserToken: ""
  
 +      # Set AnonymousUserToken to enable anonymous user access. You can get
 +      # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
 +      # in the directory where your API server is running.
 +      AnonymousUserToken: ""
 +
      AuditLogs:
        # Time to keep audit logs, in seconds. (An audit log is a row added
        # to the "logs" table in the PostgreSQL database each time an
  
        # Maximum number of log rows to delete in a single SQL transaction.
        #
 -      # If max_audit_log_delete_batch is 0, log entries will never be
 +      # If MaxDeleteBatch is 0, log entries will never be
        # deleted by Arvados. Cleanup can be done by an external process
        # without affecting any Arvados system processes, as long as very
        # recent (<5 minutes old) logs are not deleted.
        # identical to the permission key given to Keep. IMPORTANT: This is
        # a site secret. It should be at least 50 characters.
        #
 -      # Modifying blob_signing_key will invalidate all existing
 +      # Modifying BlobSigningKey will invalidate all existing
        # signatures, which can cause programs to fail (e.g., arv-put,
        # arv-get, and Crunch jobs).  To avoid errors, rotate keys only when
        # no such processes are running.
        # keepstore servers.  Otherwise, reading data blocks and saving
        # collections will fail with HTTP 403 permission errors.
        #
 -      # Modifying blob_signature_ttl invalidates existing signatures; see
 -      # blob_signing_key note above.
 +      # Modifying BlobSigningTTL invalidates existing signatures; see
 +      # BlobSigningKey note above.
        #
        # The default is 2 weeks.
        BlobSigningTTL: 336h
  
        # Default lifetime for ephemeral collections: 2 weeks. This must not
 -      # be less than blob_signature_ttl.
 +      # be less than BlobSigningTTL.
        DefaultTrashLifetime: 336h
  
        # Interval (seconds) between trash sweeps. During a trash sweep,
  
        # If true, enable collection versioning.
        # When a collection's preserve_version field is true or the current version
 -      # is older than the amount of seconds defined on preserve_version_if_idle,
 +      # is older than the amount of seconds defined on PreserveVersionIfIdle,
        # a snapshot of the collection's previous state is created and linked to
        # the current collection.
        CollectionVersioning: false
        # The default setting (false) is appropriate for a multi-user site.
        TrustAllContent: false
  
 +      # Cache parameters for WebDAV content serving:
 +      # * TTL: Maximum time to cache manifests and permission checks.
 +      # * UUIDTTL: Maximum time to cache collection state.
 +      # * MaxCollectionEntries: Maximum number of collection cache entries.
 +      # * MaxCollectionBytes: Approximate memory limit for collection cache.
 +      # * MaxPermissionEntries: Maximum number of permission cache entries.
 +      # * MaxUUIDEntries: Maximum number of UUID cache entries.
 +      WebDAVCache:
 +        TTL: 300s
 +        UUIDTTL: 5s
 +        MaxCollectionEntries: 1000
 +        MaxCollectionBytes:   100000000
 +        MaxPermissionEntries: 1000
 +        MaxUUIDEntries:       1000
 +
      Login:
        # These settings are provided by your OAuth2 provider (e.g.,
        # sso-provider).
        SLURM:
          PrioritySpread: 0
          SbatchArgumentsList: []
+         SbatchEnvironmentVariables:
+           SAMPLE: ""
          Managed:
            # Path to dns server configuration directory
            # (e.g. /etc/unbound.d/conf.d). If false, do not write any config
diff --combined lib/config/deprecated.go
index b2ed1c1df9ebab9bd625461ffcd4616c866c7519,12581ddff08123cb3026afa0d7ecf5f510311570..019979d39fe2d068c4a196d398e5111d137c35c1
@@@ -197,6 -197,14 +197,14 @@@ func loadOldClientConfig(cluster *arvad
                cluster.SystemRootToken = client.AuthToken
        }
        cluster.TLS.Insecure = client.Insecure
+       ks := ""
+       for i, u := range client.KeepServiceURIs {
+               if i > 0 {
+                       ks += " "
+               }
+               ks += u
+       }
+       cluster.Containers.SLURM.SbatchEnvironmentVariables = map[string]string{"ARVADOS_KEEP_SERVICES": ks}
  }
  
  // update config using values from an crunch-dispatch-slurm config file.
@@@ -318,73 -326,3 +326,73 @@@ func (ldr *Loader) loadOldWebsocketConf
        cfg.Clusters[cluster.ClusterID] = *cluster
        return nil
  }
 +
 +const defaultKeepWebConfigPath = "/etc/arvados/keep-web/keep-web.yml"
 +
 +type oldKeepWebConfig struct {
 +      Client *arvados.Client
 +
 +      Listen string
 +
 +      AnonymousTokens    []string
 +      AttachmentOnlyHost string
 +      TrustAllContent    bool
 +
 +      Cache struct {
 +              TTL                  arvados.Duration
 +              UUIDTTL              arvados.Duration
 +              MaxCollectionEntries int
 +              MaxCollectionBytes   int64
 +              MaxPermissionEntries int
 +              MaxUUIDEntries       int
 +      }
 +
 +      // Hack to support old command line flag, which is a bool
 +      // meaning "get actual token from environment".
 +      deprecatedAllowAnonymous bool
 +
 +      // Authorization token to be included in all health check requests.
 +      ManagementToken string
 +}
 +
 +func (ldr *Loader) loadOldKeepWebConfig(cfg *arvados.Config) error {
 +      if ldr.KeepWebPath == "" {
 +              return nil
 +      }
 +      var oc oldKeepWebConfig
 +      err := ldr.loadOldConfigHelper("keep-web", ldr.KeepWebPath, &oc)
 +      if os.IsNotExist(err) && ldr.KeepWebPath == defaultKeepWebConfigPath {
 +              return nil
 +      } else if err != nil {
 +              return err
 +      }
 +
 +      cluster, err := cfg.GetCluster("")
 +      if err != nil {
 +              return err
 +      }
 +
 +      loadOldClientConfig(cluster, oc.Client)
 +
 +      cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: oc.Listen}] = arvados.ServiceInstance{}
 +      cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: oc.Listen}] = arvados.ServiceInstance{}
 +      cluster.Services.WebDAVDownload.ExternalURL = arvados.URL{Host: oc.AttachmentOnlyHost}
 +      cluster.TLS.Insecure = oc.Client.Insecure
 +      cluster.ManagementToken = oc.ManagementToken
 +      cluster.Collections.TrustAllContent = oc.TrustAllContent
 +      cluster.Collections.WebDAVCache.TTL = oc.Cache.TTL
 +      cluster.Collections.WebDAVCache.UUIDTTL = oc.Cache.UUIDTTL
 +      cluster.Collections.WebDAVCache.MaxCollectionEntries = oc.Cache.MaxCollectionEntries
 +      cluster.Collections.WebDAVCache.MaxCollectionBytes = oc.Cache.MaxCollectionBytes
 +      cluster.Collections.WebDAVCache.MaxPermissionEntries = oc.Cache.MaxPermissionEntries
 +      cluster.Collections.WebDAVCache.MaxUUIDEntries = oc.Cache.MaxUUIDEntries
 +      if len(oc.AnonymousTokens) > 0 {
 +              cluster.Users.AnonymousUserToken = oc.AnonymousTokens[0]
 +              if len(oc.AnonymousTokens) > 1 {
 +                      ldr.Logger.Warn("More than 1 anonymous tokens configured, using only the first and discarding the rest.")
 +              }
 +      }
 +
 +      cfg.Clusters[cluster.ClusterID] = *cluster
 +      return nil
 +}
diff --combined lib/config/export.go
index cdc8539f108db2109fbf189e76e14547f13bf734,b125d7dc917db54283ecf98eddf9daade25d29f8..f6b19db252f8292eb4cace79cdf55f88d56f5701
@@@ -26,6 -26,10 +26,10 @@@ func ExportJSON(w io.Writer, cluster *a
        if err != nil {
                return err
        }
+       // ClusterID is not marshalled by default (see `json:"-"`).
+       // Add it back here so it is included in the exported config.
+       m["ClusterID"] = cluster.ClusterID
        err = redactUnsafe(m, "", "")
        if err != nil {
                return err
@@@ -55,6 -59,7 +59,7 @@@
  // exists.
  var whitelist = map[string]bool{
        // | sort -t'"' -k2,2
+       "ClusterID":                                    true,
        "API":                                          true,
        "API.AsyncPermissionsUpdateInterval":           false,
        "API.DisabledAPIs":                             false,
@@@ -84,7 -89,6 +89,7 @@@
        "Collections.PreserveVersionIfIdle":            true,
        "Collections.TrashSweepInterval":               false,
        "Collections.TrustAllContent":                  false,
 +      "Collections.WebDAVCache":                      false,
        "Containers":                                   true,
        "Containers.CloudVMs":                          false,
        "Containers.CrunchRunCommand":                  false,
index e3509e4976282cf16ede51665d0667fb3957f4a5,602f30e1dae5480bb22ed39e3b0a9bf8c1e04e8f..14bbf33dbdfd7142c78f08b90d971d072b090aa4
@@@ -218,8 -218,8 +218,8 @@@ Clusters
        # to run an open instance where anyone can create an account and use
        # the system without requiring manual approval.
        #
 -      # The params auto_setup_new_users_with_* are meaningful only when auto_setup_new_users is turned on.
 -      # auto_setup_name_blacklist is a list of usernames to be blacklisted for auto setup.
 +      # The params AutoSetupNewUsersWith* are meaningful only when AutoSetupNewUsers is turned on.
 +      # AutoSetupUsernameBlacklist is a list of usernames to be blacklisted for auto setup.
        AutoSetupNewUsers: false
        AutoSetupNewUsersWithVmUUID: ""
        AutoSetupNewUsersWithRepository: false
          syslog: {}
          SAMPLE: {}
  
 -      # When new_users_are_active is set to true, new users will be active
 +      # When NewUsersAreActive is set to true, new users will be active
        # immediately.  This skips the "self-activate" step which enforces
        # user agreements.  Should only be enabled for development.
        NewUsersAreActive: false
        # should be an address associated with a Google account.
        AutoAdminUserWithEmail: ""
  
 -      # If auto_admin_first_user is set to true, the first user to log in when no
 +      # If AutoAdminFirstUser is set to true, the first user to log in when no
        # other admin users exist will automatically become an admin user.
        AutoAdminFirstUser: false
  
        # in the directory where your API server is running.
        AnonymousUserToken: ""
  
 +      # Set AnonymousUserToken to enable anonymous user access. You can get
 +      # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
 +      # in the directory where your API server is running.
 +      AnonymousUserToken: ""
 +
      AuditLogs:
        # Time to keep audit logs, in seconds. (An audit log is a row added
        # to the "logs" table in the PostgreSQL database each time an
  
        # Maximum number of log rows to delete in a single SQL transaction.
        #
 -      # If max_audit_log_delete_batch is 0, log entries will never be
 +      # If MaxDeleteBatch is 0, log entries will never be
        # deleted by Arvados. Cleanup can be done by an external process
        # without affecting any Arvados system processes, as long as very
        # recent (<5 minutes old) logs are not deleted.
        # identical to the permission key given to Keep. IMPORTANT: This is
        # a site secret. It should be at least 50 characters.
        #
 -      # Modifying blob_signing_key will invalidate all existing
 +      # Modifying BlobSigningKey will invalidate all existing
        # signatures, which can cause programs to fail (e.g., arv-put,
        # arv-get, and Crunch jobs).  To avoid errors, rotate keys only when
        # no such processes are running.
        # keepstore servers.  Otherwise, reading data blocks and saving
        # collections will fail with HTTP 403 permission errors.
        #
 -      # Modifying blob_signature_ttl invalidates existing signatures; see
 -      # blob_signing_key note above.
 +      # Modifying BlobSigningTTL invalidates existing signatures; see
 +      # BlobSigningKey note above.
        #
        # The default is 2 weeks.
        BlobSigningTTL: 336h
  
        # Default lifetime for ephemeral collections: 2 weeks. This must not
 -      # be less than blob_signature_ttl.
 +      # be less than BlobSigningTTL.
        DefaultTrashLifetime: 336h
  
        # Interval (seconds) between trash sweeps. During a trash sweep,
  
        # If true, enable collection versioning.
        # When a collection's preserve_version field is true or the current version
 -      # is older than the amount of seconds defined on preserve_version_if_idle,
 +      # is older than the amount of seconds defined on PreserveVersionIfIdle,
        # a snapshot of the collection's previous state is created and linked to
        # the current collection.
        CollectionVersioning: false
        # The default setting (false) is appropriate for a multi-user site.
        TrustAllContent: false
  
 +      # Cache parameters for WebDAV content serving:
 +      # * TTL: Maximum time to cache manifests and permission checks.
 +      # * UUIDTTL: Maximum time to cache collection state.
 +      # * MaxCollectionEntries: Maximum number of collection cache entries.
 +      # * MaxCollectionBytes: Approximate memory limit for collection cache.
 +      # * MaxPermissionEntries: Maximum number of permission cache entries.
 +      # * MaxUUIDEntries: Maximum number of UUID cache entries.
 +      WebDAVCache:
 +        TTL: 300s
 +        UUIDTTL: 5s
 +        MaxCollectionEntries: 1000
 +        MaxCollectionBytes:   100000000
 +        MaxPermissionEntries: 1000
 +        MaxUUIDEntries:       1000
 +
      Login:
        # These settings are provided by your OAuth2 provider (e.g.,
        # sso-provider).
        SLURM:
          PrioritySpread: 0
          SbatchArgumentsList: []
+         SbatchEnvironmentVariables:
+           SAMPLE: ""
          Managed:
            # Path to dns server configuration directory
            # (e.g. /etc/unbound.d/conf.d). If false, do not write any config
diff --combined sdk/go/arvados/config.go
index 6384401396275390540845ece7c4e24db1589f49,f6b736d587c893bc8128875e5889ebfe0ad78be7..a5cf25b8c9217689b6e4ebfe7926c043a0213612
@@@ -57,14 -57,6 +57,14 @@@ func (sc *Config) GetCluster(clusterID 
        }
  }
  
 +type WebDAVCacheConfig struct {
 +      TTL                  Duration
 +      UUIDTTL              Duration
 +      MaxCollectionEntries int
 +      MaxCollectionBytes   int64
 +      MaxPermissionEntries int
 +      MaxUUIDEntries       int
 +}
  type Cluster struct {
        ClusterID       string `json:"-"`
        ManagementToken string
                PreserveVersionIfIdle Duration
                TrashSweepInterval    Duration
                TrustAllContent       bool
 +
 +              WebDAVCache WebDAVCacheConfig
        }
        Git struct {
                Repositories string
@@@ -302,9 -292,10 +302,10 @@@ type ContainersConfig struct 
                LogUpdateSize                ByteSize
        }
        SLURM struct {
-               PrioritySpread      int64
-               SbatchArgumentsList []string
-               Managed             struct {
+               PrioritySpread             int64
+               SbatchArgumentsList        []string
+               SbatchEnvironmentVariables map[string]string
+               Managed                    struct {
                        DNSServerConfDir       string
                        DNSServerConfTemplate  string
                        DNSServerReloadCommand string
index fab004c89f23fce759c8386b42f6db5d46023df1,837579fe25acfbff5283b28bbb7f4375a3322280..863b91a7e1beecae13635cb0e89c830bb264faac
@@@ -81,7 -81,7 +81,7 @@@ func (h *handler) setup() 
        keepclient.RefreshServiceDiscoveryOnSIGHUP()
  
        h.healthHandler = &health.Handler{
 -              Token:  h.Config.ManagementToken,
 +              Token:  h.Config.cluster.ManagementToken,
                Prefix: "/_health/",
        }
  
@@@ -249,9 -249,9 +249,9 @@@ func (h *handler) ServeHTTP(wOrig http.
        var pathToken bool
        var attachment bool
        var useSiteFS bool
 -      credentialsOK := h.Config.TrustAllContent
 +      credentialsOK := h.Config.cluster.Collections.TrustAllContent
  
 -      if r.Host != "" && r.Host == h.Config.AttachmentOnlyHost {
 +      if r.Host != "" && r.Host == h.Config.cluster.Services.WebDAVDownload.ExternalURL.Host {
                credentialsOK = true
                attachment = true
        } else if r.FormValue("disposition") == "attachment" {
                        stripParts = 4
                        pathToken = true
                } else {
-                       log.Info("  !!!!  ATTN: Into /collections/uuid/path with anon token: ", h.Config.cluster.Users.AnonymousUserToken)
                        // /collections/ID/PATH...
                        collectionID = parseCollectionIDFromURL(pathParts[1])
-                       tokens = []string{h.Config.cluster.Users.AnonymousUserToken}
                        stripParts = 2
+                       // This path is only meant to work for public
+                       // data. Tokens provided with the request are
+                       // ignored.
+                       credentialsOK = false
                }
        }
  
                forceReload = true
        }
  
+       if credentialsOK {
+               reqTokens = auth.CredentialsFromRequest(r).Tokens
+       }
        formToken := r.FormValue("api_token")
        if formToken != "" && r.Header.Get("Origin") != "" && attachment && r.URL.Query().Get("api_token") == "" {
                // The client provided an explicit token in the POST
                //
                // * The token isn't embedded in the URL, so we don't
                //   need to worry about bookmarks and copy/paste.
-               tokens = append(tokens, formToken)
+               reqTokens = append(reqTokens, formToken)
        } else if formToken != "" && browserMethod[r.Method] {
                // The client provided an explicit token in the query
                // string, or a form in POST body. We must put the
        }
  
        if useSiteFS {
-               if tokens == nil {
-                       tokens = auth.CredentialsFromRequest(r).Tokens
-               }
-               h.serveSiteFS(w, r, tokens, credentialsOK, attachment)
+               h.serveSiteFS(w, r, reqTokens, credentialsOK, attachment)
                return
        }
  
        }
  
        if tokens == nil {
-               if credentialsOK {
-                       reqTokens = auth.CredentialsFromRequest(r).Tokens
-               }
 -              tokens = append(reqTokens, h.Config.AnonymousTokens...)
 +              tokens = append(reqTokens, h.Config.cluster.Users.AnonymousUserToken)
        }
  
        if len(targetPath) > 0 && targetPath[0] == "_" {
index 17719601a9c7b76147b4abefea4edf7db3eff472,dd91df354900175592501ce794a6d9dc46cf8f41..fe8e767c4c63aa9d51d390220b7c5fe14173c63e
@@@ -17,7 -17,6 +17,7 @@@ import 
        "regexp"
        "strings"
  
 +      "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        "git.curoverse.com/arvados.git/sdk/go/auth"
  
  var _ = check.Suite(&UnitSuite{})
  
 -type UnitSuite struct{}
 +type UnitSuite struct {
 +      Config *arvados.Config
 +}
 +
 +func (s *UnitSuite) SetUpTest(c *check.C) {
 +      ldr := config.NewLoader(nil, nil)
 +      cfg, err := ldr.LoadDefaults()
 +      c.Assert(err, check.IsNil)
 +      s.Config = cfg
 +}
  
  func (s *UnitSuite) TestCORSPreflight(c *check.C) {
 -      h := handler{Config: DefaultConfig()}
 +      h := handler{Config: DefaultConfig(s.Config)}
        u := mustParseURL("http://keep-web.example/c=" + arvadostest.FooCollection + "/foo")
        req := &http.Request{
                Method:     "OPTIONS",
@@@ -88,8 -78,8 +88,8 @@@ func (s *UnitSuite) TestInvalidUUID(c *
                        RequestURI: u.RequestURI(),
                }
                resp := httptest.NewRecorder()
 -              cfg := DefaultConfig()
 -              cfg.AnonymousTokens = []string{arvadostest.AnonymousToken}
 +              cfg := DefaultConfig(s.Config)
 +              cfg.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
                h := handler{Config: cfg}
                h.ServeHTTP(resp, req)
                c.Check(resp.Code, check.Equals, http.StatusNotFound)
@@@ -348,7 -338,7 +348,7 @@@ func (s *IntegrationSuite) TestVhostRed
  }
  
  func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = "download.example.com"
 +      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "download.example.com/by_id/"+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
  }
  
  func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = "download.example.com"
 +      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
                "?api_token="+arvadostest.ActiveToken,
  }
  
  func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
 -      s.testServer.Config.TrustAllContent = true
 +      s.testServer.Config.cluster.Collections.TrustAllContent = true
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
  }
  
  func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
 +      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com:1234"
  
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
@@@ -440,7 -430,7 +440,7 @@@ func (s *IntegrationSuite) TestVhostRed
  }
  
  func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
 -      s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
 +      s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
                "",
  }
  
  func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
 -      s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
 +      s.testServer.Config.cluster.Users.AnonymousUserToken = "anonymousTokenConfiguredButInvalid"
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
                "",
  }
  
  func (s *IntegrationSuite) TestSpecialCharsInPath(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = "download.example.com"
 +      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
  
        client := s.testServer.Config.Client
        client.AuthToken = arvadostest.ActiveToken
@@@ -569,8 -559,18 +569,18 @@@ func (s *IntegrationSuite) testVhostRed
        return resp
  }
  
- func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
+ func (s *IntegrationSuite) TestDirectoryListingWithAnonymousToken(c *check.C) {
 -      s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
++      s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
+       s.testDirectoryListing(c)
+ }
+ func (s *IntegrationSuite) TestDirectoryListingWithNoAnonymousToken(c *check.C) {
 -      s.testServer.Config.AnonymousTokens = nil
++      s.testServer.Config.cluster.Users.AnonymousUserToken = ""
+       s.testDirectoryListing(c)
+ }
+ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = "download.example.com"
 +      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
        authHeader := http.Header{
                "Authorization": {"OAuth2 " + arvadostest.ActiveToken},
        }
                        expect:  []string{"foo", "bar"},
                        cutDirs: 1,
                },
-               // This test case fails
                {
-                       uri:     "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
-                       header:  authHeader,
-                       expect:  []string{"dir1/foo", "dir1/bar"},
-                       cutDirs: 2,
+                       // URLs of this form ignore authHeader, and
+                       // FooAndBarFilesInDirUUID isn't public, so
+                       // this returns 404.
+                       uri:    "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
+                       header: authHeader,
+                       expect: nil,
                },
                {
                        uri:     "download.example.com/users/active/foo_file_in_dir/",
                        cutDirs: 2,
                },
        } {
 -              c.Logf("HTML: %q => %q", trial.uri, trial.expect)
 +              comment := check.Commentf("HTML: %q => %q", trial.uri, trial.expect)
                resp := httptest.NewRecorder()
                u := mustParseURL("//" + trial.uri)
                req := &http.Request{
                        s.testServer.Handler.ServeHTTP(resp, req)
                }
                if trial.redirect != "" {
 -                      c.Check(req.URL.Path, check.Equals, trial.redirect)
 +                      c.Check(req.URL.Path, check.Equals, trial.redirect, comment)
                }
                if trial.expect == nil {
 -                      c.Check(resp.Code, check.Equals, http.StatusNotFound)
 +                      c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
                } else {
 -                      c.Check(resp.Code, check.Equals, http.StatusOK)
 +                      c.Check(resp.Code, check.Equals, http.StatusOK, comment)
                        for _, e := range trial.expect {
 -                              c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./`+e+`".*`)
 +                              c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./`+e+`".*`, comment)
                        }
 -                      c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`)
 +                      c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`, comment)
                }
  
 -              c.Logf("WebDAV: %q => %q", trial.uri, trial.expect)
 +              comment = check.Commentf("WebDAV: %q => %q", trial.uri, trial.expect)
                req = &http.Request{
                        Method:     "OPTIONS",
                        Host:       u.Host,
                resp = httptest.NewRecorder()
                s.testServer.Handler.ServeHTTP(resp, req)
                if trial.expect == nil {
 -                      c.Check(resp.Code, check.Equals, http.StatusNotFound)
 +                      c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
                } else {
 -                      c.Check(resp.Code, check.Equals, http.StatusOK)
 +                      c.Check(resp.Code, check.Equals, http.StatusOK, comment)
                }
  
                req = &http.Request{
                resp = httptest.NewRecorder()
                s.testServer.Handler.ServeHTTP(resp, req)
                if trial.expect == nil {
 -                      c.Check(resp.Code, check.Equals, http.StatusNotFound)
 +                      c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
                } else {
 -                      c.Check(resp.Code, check.Equals, http.StatusMultiStatus)
 +                      c.Check(resp.Code, check.Equals, http.StatusMultiStatus, comment)
                        for _, e := range trial.expect {
 -                              c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+filepath.Join(u.Path, e)+`</D:href>.*`)
 +                              c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+filepath.Join(u.Path, e)+`</D:href>.*`, comment)
                        }
                }
        }
@@@ -801,7 -802,7 +812,7 @@@ func (s *IntegrationSuite) TestDeleteLa
  
        var updated arvados.Collection
        for _, fnm := range []string{"foo.txt", "bar.txt"} {
 -              s.testServer.Config.AttachmentOnlyHost = "example.com"
 +              s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "example.com"
                u, _ := url.Parse("http://example.com/c=" + newCollection.UUID + "/" + fnm)
                req := &http.Request{
                        Method:     "DELETE",
  }
  
  func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) {
 -      s.testServer.Config.ManagementToken = arvadostest.ManagementToken
 +      s.testServer.Config.cluster.ManagementToken = arvadostest.ManagementToken
        authHeader := http.Header{
                "Authorization": {"Bearer " + arvadostest.ManagementToken},
        }
index b398f34065e2039941e17660a20d42497819203d,0263dcf08f92c906032664c8b0d3b6de8726d9b7..12596b16bb1bd3edce95bc8daf63dac100819e15
@@@ -16,14 -16,11 +16,14 @@@ import 
        "os/exec"
        "strings"
        "testing"
 +      "time"
  
 +      "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
 +      log "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
  )
  
@@@ -151,7 -148,7 +151,7 @@@ type curlCase struct 
  }
  
  func (s *IntegrationSuite) Test200(c *check.C) {
 -      s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
 +      s.testServer.Config.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
        for _, spec := range []curlCase{
                // My collection
                {
@@@ -301,6 -298,7 +301,7 @@@ func (s *IntegrationSuite) runCurl(c *c
  }
  
  func (s *IntegrationSuite) TestMetrics(c *check.C) {
 -      s.testServer.Config.AttachmentOnlyHost = s.testServer.Addr
++      s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = s.testServer.Addr
        origin := "http://" + s.testServer.Addr
        req, _ := http.NewRequest("GET", origin+"/notfound", nil)
        _, err := http.DefaultClient.Do(req)
@@@ -422,66 -420,6 +423,66 @@@ func (s *IntegrationSuite) SetUpSuite(
        kc.PutB([]byte("waz"))
  }
  
 +func (s *UnitSuite) TestLegacyConfig(c *check.C) {
 +      content := []byte(`
 +{
 +      "Client": {
 +              "Scheme": "",
 +              "APIHost": "example.com",
 +              "AuthToken": "abcdefg",
 +      },
 +      "Listen": ":80",
 +      "AnonymousTokens": [
 +              "anonusertoken"
 +      ],
 +      "AttachmentOnlyHost": "download.example.com",
 +      "TrustAllContent": true,
 +      "Cache": {
 +              "TTL": "1m",
 +              "UUIDTTL": "1s",
 +              "MaxCollectionEntries": 42,
 +              "MaxCollectionBytes": 1234567890,
 +              "MaxPermissionEntries": 100,
 +              "MaxUUIDEntries": 100
 +      },
 +      "ManagementToken": "xyzzy"
 +}
 +`)
 +      tmpfile, err := ioutil.TempFile("", "example")
 +      if err != nil {
 +              c.Error(err)
 +      }
 +      defer os.Remove(tmpfile.Name())
 +
 +      if _, err := tmpfile.Write(content); err != nil {
 +              c.Error(err)
 +      }
 +      if err := tmpfile.Close(); err != nil {
 +              c.Error(err)
 +      }
 +      cfg := configure(log.New(), []string{"keep-web", "-config", tmpfile.Name()})
 +      c.Check(cfg, check.NotNil)
 +      c.Check(cfg.cluster, check.NotNil)
 +
 +      c.Check(cfg.cluster.Services.Controller.ExternalURL, check.Equals, arvados.URL{Scheme: "https", Host: "example.com"})
 +      c.Check(cfg.cluster.SystemRootToken, check.Equals, "abcdefg")
 +
 +      c.Check(cfg.cluster.Collections.WebDAVCache.TTL, check.Equals, arvados.Duration(60*time.Second))
 +      c.Check(cfg.cluster.Collections.WebDAVCache.UUIDTTL, check.Equals, arvados.Duration(time.Second))
 +      c.Check(cfg.cluster.Collections.WebDAVCache.MaxCollectionEntries, check.Equals, 42)
 +      c.Check(cfg.cluster.Collections.WebDAVCache.MaxCollectionBytes, check.Equals, int64(1234567890))
 +      c.Check(cfg.cluster.Collections.WebDAVCache.MaxPermissionEntries, check.Equals, 100)
 +      c.Check(cfg.cluster.Collections.WebDAVCache.MaxUUIDEntries, check.Equals, 100)
 +
 +      c.Check(cfg.cluster.Services.WebDAVDownload.ExternalURL, check.Equals, arvados.URL{Host: "download.example.com"})
 +      c.Check(cfg.cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: ":80"}], check.NotNil)
 +      c.Check(cfg.cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: ":80"}], check.NotNil)
 +
 +      c.Check(cfg.cluster.Collections.TrustAllContent, check.Equals, true)
 +      c.Check(cfg.cluster.Users.AnonymousUserToken, check.Equals, "anonusertoken")
 +      c.Check(cfg.cluster.ManagementToken, check.Equals, "xyzzy")
 +}
 +
  func (s *IntegrationSuite) TearDownSuite(c *check.C) {
        arvadostest.StopKeep(2)
        arvadostest.StopAPI()
  
  func (s *IntegrationSuite) SetUpTest(c *check.C) {
        arvadostest.ResetEnv()
 -      cfg := DefaultConfig()
 +      ldr := config.NewLoader(nil, nil)
 +      arvCfg, err := ldr.LoadDefaults()
 +      cfg := DefaultConfig(arvCfg)
 +      c.Assert(err, check.IsNil)
        cfg.Client = arvados.Client{
                APIHost:  testAPIHost,
                Insecure: true,
        }
 -      cfg.Listen = "127.0.0.1:0"
 -      cfg.ManagementToken = arvadostest.ManagementToken
 +      listen := "127.0.0.1:0"
 +      cfg.cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: listen}] = arvados.ServiceInstance{}
 +      cfg.cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: listen}] = arvados.ServiceInstance{}
 +      cfg.cluster.ManagementToken = arvadostest.ManagementToken
 +      cfg.cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken
        s.testServer = &server{Config: cfg}
 -      err := s.testServer.Start()
 +      err = s.testServer.Start()
        c.Assert(err, check.Equals, nil)
  }