}
var contents CollectionList
- err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
- Count: "none",
- Filters: []Filter{
- {"name", "=", name},
- {"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
- {"groups.group_class", "=", "project"},
- },
- })
- if err != nil {
- return nil, err
+ for _, subst := range []string{"/", fs.forwardSlashNameSubstitution} {
+ contents = CollectionList{}
+ err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
+ Count: "none",
+ Filters: []Filter{
+ {"name", "=", strings.Replace(name, subst, "/", -1)},
+ {"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
+ {"groups.group_class", "=", "project"},
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(contents.Items) > 0 || fs.forwardSlashNameSubstitution == "/" || fs.forwardSlashNameSubstitution == "" || !strings.Contains(name, fs.forwardSlashNameSubstitution) {
+ break
+ }
+ // If the requested name contains the configured "/"
+ // replacement string and didn't match a
+ // project/collection exactly, we'll try again with
+ // "/" in its place, so a lookup of a munged name
+ // works regardless of whether the directory listing
+ // has been populated with escaped names.
+ //
+ // Note this doesn't handle items whose names contain
+ // both "/" and the substitution string.
}
if len(contents.Items) == 0 {
return nil, os.ErrNotExist
}
for _, i := range resp.Items {
coll := i
+ if fs.forwardSlashNameSubstitution != "" {
+ coll.Name = strings.Replace(coll.Name, "/", fs.forwardSlashNameSubstitution, -1)
+ }
if !permittedName(coll.Name) {
continue
}
break
}
for _, group := range resp.Items {
+ if fs.forwardSlashNameSubstitution != "" {
+ group.Name = strings.Replace(group.Name, "/", fs.forwardSlashNameSubstitution, -1)
+ }
if !permittedName(group.Name) {
continue
}
c.Logf("fi.Name() == %q", fi.Name())
c.Check(strings.Contains(fi.Name(), "/"), check.Equals, false)
}
+
+ // Make a new fs (otherwise content will still be cached from
+ // above) and enable "/" replacement string.
+ s.fs = s.client.SiteFileSystem(s.kc)
+ s.fs.ForwardSlashNameSubstitution("___")
+ dir, err = s.fs.Open("/users/active/A Project/bad___collection")
+ if c.Check(err, check.IsNil) {
+ _, err = dir.Readdir(-1)
+ c.Check(err, check.IsNil)
+ }
+ dir, err = s.fs.Open("/users/active/A Project/bad___project")
+ if c.Check(err, check.IsNil) {
+ _, err = dir.Readdir(-1)
+ c.Check(err, check.IsNil)
+ }
}
func (s *SiteFSSuite) TestProjectUpdatedByOther(c *check.C) {
MountByID(mount string)
MountProject(mount, uuid string)
MountUsers(mount string)
+ ForwardSlashNameSubstitution(string)
}
type customFileSystem struct {
staleThreshold time.Time
staleLock sync.Mutex
+
+ forwardSlashNameSubstitution string
}
func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
})
}
+func (fs *customFileSystem) ForwardSlashNameSubstitution(repl string) {
+ fs.forwardSlashNameSubstitution = repl
+}
+
// SiteFileSystem returns a FileSystem that maps collections and other
// Arvados objects onto a filesystem layout.
//
"Collections": {
"BlobSigningKey": "zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc",
"TrustAllContent": True,
+ "ForwardSlashNameSubstitution": "/",
},
"Git": {
"Repositories": "%s/test" % os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git'),
Insecure: arv.ApiInsecure,
}).WithRequestID(r.Header.Get("X-Request-Id"))
fs := client.SiteFileSystem(kc)
+ fs.ForwardSlashNameSubstitution(h.Config.cluster.Collections.ForwardSlashNameSubstitution)
f, err := fs.Open(r.URL.Path)
if os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusNotFound)
c.Check(resp.Body.String(), check.Matches, `(?ms).*href="./https:%5c%22odd%27%20path%20chars"\S+https:\\"odd' path chars.*`)
}
+func (s *IntegrationSuite) TestForwardSlashSubstitution(c *check.C) {
+ arv := arvados.NewClientFromEnv()
+ s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = "download.example.com"
+ s.testServer.Config.cluster.Collections.ForwardSlashNameSubstitution = "{SOLIDUS}"
+ name := "foo/bar/baz"
+ nameShown := strings.Replace(name, "/", "{SOLIDUS}", -1)
+ nameShownEscaped := strings.Replace(name, "/", "%7bSOLIDUS%7d", -1)
+
+ client := s.testServer.Config.Client
+ client.AuthToken = arvadostest.ActiveToken
+ fs, err := (&arvados.Collection{}).FileSystem(&client, nil)
+ c.Assert(err, check.IsNil)
+ f, err := fs.OpenFile("filename", os.O_CREATE, 0777)
+ c.Assert(err, check.IsNil)
+ f.Close()
+ mtxt, err := fs.MarshalManifest(".")
+ c.Assert(err, check.IsNil)
+ var coll arvados.Collection
+ err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+ "collection": map[string]string{
+ "manifest_text": mtxt,
+ "name": name,
+ "owner_uuid": arvadostest.AProjectUUID,
+ },
+ })
+ c.Assert(err, check.IsNil)
+ defer arv.RequestAndDecode(&coll, "DELETE", "arvados/v1/collections/"+coll.UUID, nil, nil)
+
+ base := "http://download.example.com/by_id/" + coll.OwnerUUID + "/"
+ for tryURL, expectRegexp := range map[string]string{
+ base: `(?ms).*href="./` + nameShownEscaped + `/"\S+` + nameShown + `.*`,
+ base + nameShown + "/": `(?ms).*href="./filename"\S+filename.*`,
+ } {
+ u, _ := url.Parse(tryURL)
+ req := &http.Request{
+ Method: "GET",
+ Host: u.Host,
+ URL: u,
+ RequestURI: u.RequestURI(),
+ Header: http.Header{
+ "Authorization": {"Bearer " + client.AuthToken},
+ },
+ }
+ resp := httptest.NewRecorder()
+ s.testServer.Handler.ServeHTTP(resp, req)
+ c.Check(resp.Code, check.Equals, http.StatusOK)
+ c.Check(resp.Body.String(), check.Matches, expectRegexp)
+ }
+}
+
// XHRs can't follow redirect-with-cookie so they rely on method=POST
// and disposition=attachment (telling us it's acceptable to respond
// with content instead of a redirect) and an Origin header that gets