21566: Fix V4 signature check for paths with double slash.
[arvados.git] / services / keep-web / cadaver_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package keepweb
6
7 import (
8         "bytes"
9         "fmt"
10         "io"
11         "io/ioutil"
12         "os"
13         "os/exec"
14         "path/filepath"
15         "strings"
16         "time"
17
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "git.arvados.org/arvados.git/sdk/go/arvadostest"
20         check "gopkg.in/check.v1"
21 )
22
23 func (s *IntegrationSuite) TestCadaverHTTPAuth(c *check.C) {
24         s.testCadaver(c, arvadostest.ActiveToken, func(newCollection arvados.Collection) (string, string, string) {
25                 r := "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/"
26                 w := "/c=" + newCollection.UUID + "/"
27                 pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/"
28                 return r, w, pdh
29         }, nil)
30 }
31
32 func (s *IntegrationSuite) TestCadaverPathAuth(c *check.C) {
33         s.testCadaver(c, "", func(newCollection arvados.Collection) (string, string, string) {
34                 r := "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/"
35                 w := "/c=" + newCollection.UUID + "/t=" + arvadostest.ActiveToken + "/"
36                 pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/t=" + arvadostest.ActiveToken + "/"
37                 return r, w, pdh
38         }, nil)
39 }
40
41 func (s *IntegrationSuite) TestCadaverUserProject(c *check.C) {
42         rpath := "/users/active/foo_file_in_dir/"
43         s.testCadaver(c, arvadostest.ActiveToken, func(newCollection arvados.Collection) (string, string, string) {
44                 wpath := "/users/active/" + newCollection.Name
45                 pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/"
46                 return rpath, wpath, pdh
47         }, func(path string) bool {
48                 // Skip tests that rely on writes, because /users/
49                 // tree is read-only.
50                 return !strings.HasPrefix(path, rpath) || strings.HasPrefix(path, rpath+"_/")
51         })
52 }
53
54 func (s *IntegrationSuite) testCadaver(c *check.C, password string, pathFunc func(arvados.Collection) (string, string, string), skip func(string) bool) {
55         testdata := "the human tragedy consists in the necessity of living with the consequences of actions performed under the pressure of compulsions we do not understand"
56
57         tempdir, err := ioutil.TempDir("", "keep-web-test-")
58         c.Assert(err, check.IsNil)
59         defer os.RemoveAll(tempdir)
60
61         localfile, err := ioutil.TempFile(tempdir, "localfile")
62         c.Assert(err, check.IsNil)
63         localfile.Write([]byte(testdata))
64
65         emptyfile, err := ioutil.TempFile(tempdir, "emptyfile")
66         c.Assert(err, check.IsNil)
67
68         checkfile, err := ioutil.TempFile(tempdir, "checkfile")
69         c.Assert(err, check.IsNil)
70
71         var newCollection arvados.Collection
72         arv := arvados.NewClientFromEnv()
73         arv.AuthToken = arvadostest.ActiveToken
74         err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{}})
75         c.Assert(err, check.IsNil)
76
77         readPath, writePath, pdhPath := pathFunc(newCollection)
78
79         matchToday := time.Now().Format("Jan +2")
80
81         type testcase struct {
82                 path           string
83                 cmd            string
84                 match          string
85                 data           string
86                 checkemptydata bool
87         }
88         for _, trial := range []testcase{
89                 {
90                         path:  readPath,
91                         cmd:   "ls\n",
92                         match: `(?ms).*dir1 *0 .*`,
93                 },
94                 {
95                         path:  readPath,
96                         cmd:   "ls dir1\n",
97                         match: `(?ms).*bar *3.*foo *3 .*`,
98                 },
99                 {
100                         path:  readPath + "_/dir1",
101                         cmd:   "ls\n",
102                         match: `(?ms).*bar *3.*foo *3 .*`,
103                 },
104                 {
105                         path:  readPath + "dir1/",
106                         cmd:   "ls\n",
107                         match: `(?ms).*bar *3.*foo +3 +Feb +\d+ +2014.*`,
108                 },
109                 {
110                         path:  writePath,
111                         cmd:   "get emptyfile '" + checkfile.Name() + "'\n",
112                         match: `(?ms).*Not Found.*`,
113                 },
114                 {
115                         path:  writePath,
116                         cmd:   "put '" + emptyfile.Name() + "' emptyfile\n",
117                         match: `(?ms).*Uploading .* succeeded.*`,
118                 },
119                 {
120                         path:           writePath,
121                         cmd:            "get emptyfile '" + checkfile.Name() + "'\n",
122                         match:          `(?ms).*Downloading .* succeeded.*`,
123                         checkemptydata: true,
124                 },
125                 {
126                         path:  writePath,
127                         cmd:   "put '" + localfile.Name() + "' testfile\n",
128                         match: `(?ms).*Uploading .* succeeded.*`,
129                 },
130                 {
131                         path:  writePath,
132                         cmd:   "get testfile '" + checkfile.Name() + "'\n",
133                         match: `(?ms).*succeeded.*`,
134                         data:  testdata,
135                 },
136                 {
137                         path:  writePath,
138                         cmd:   "move testfile \"test &#!%20 file\"\n",
139                         match: `(?ms).*Moving .* succeeded.*`,
140                 },
141                 {
142                         path:  writePath,
143                         cmd:   "move \"test &#!%20 file\" testfile\n",
144                         match: `(?ms).*Moving .* succeeded.*`,
145                 },
146                 {
147                         path:  writePath,
148                         cmd:   "mkcol newdir0/\n",
149                         match: `(?ms).*Creating .* succeeded.*`,
150                 },
151                 {
152                         path:  writePath,
153                         cmd:   "move testfile newdir0/\n",
154                         match: `(?ms).*Moving .* succeeded.*`,
155                 },
156                 {
157                         path:  writePath,
158                         cmd:   "move testfile newdir0/\n",
159                         match: `(?ms).*Moving .* failed.*`,
160                 },
161                 {
162                         path:  writePath,
163                         cmd:   "lock newdir0/testfile\n",
164                         match: `(?ms).*Locking .* succeeded.*`,
165                 },
166                 {
167                         path:  writePath,
168                         cmd:   "unlock newdir0/testfile\nasdf\n",
169                         match: `(?ms).*Unlocking .* succeeded.*`,
170                 },
171                 {
172                         path:  writePath,
173                         cmd:   "ls\n",
174                         match: `(?ms).*newdir0.* 0 +` + matchToday + ` \d+:\d+\n.*`,
175                 },
176                 {
177                         path:  writePath,
178                         cmd:   "move newdir0/testfile emptyfile/bogus/\n",
179                         match: `(?ms).*Moving .* failed.*`,
180                 },
181                 {
182                         path:  writePath,
183                         cmd:   "mkcol newdir1\n",
184                         match: `(?ms).*Creating .* succeeded.*`,
185                 },
186                 {
187                         path:  writePath,
188                         cmd:   "move newdir1/ newdir1x/\n",
189                         match: `(?ms).*Moving .* succeeded.*`,
190                 },
191                 {
192                         path:  writePath,
193                         cmd:   "move newdir1x newdir1\n",
194                         match: `(?ms).*Moving .* succeeded.*`,
195                 },
196                 {
197                         path:  writePath,
198                         cmd:   "move newdir0/testfile newdir1/\n",
199                         match: `(?ms).*Moving .* succeeded.*`,
200                 },
201                 {
202                         path:  writePath,
203                         cmd:   "move newdir1 newdir1/\n",
204                         match: `(?ms).*Moving .* failed.*`,
205                 },
206                 {
207                         path:  writePath,
208                         cmd:   "get newdir1/testfile '" + checkfile.Name() + "'\n",
209                         match: `(?ms).*succeeded.*`,
210                         data:  testdata,
211                 },
212                 {
213                         path:  writePath,
214                         cmd:   "put '" + localfile.Name() + "' newdir1/testfile1\n",
215                         match: `(?ms).*Uploading .* succeeded.*`,
216                 },
217                 {
218                         path:  writePath,
219                         cmd:   "mkcol newdir2\n",
220                         match: `(?ms).*Creating .* succeeded.*`,
221                 },
222                 {
223                         path:  writePath,
224                         cmd:   "put '" + localfile.Name() + "' newdir2/testfile2\n",
225                         match: `(?ms).*Uploading .* succeeded.*`,
226                 },
227                 {
228                         path:  writePath,
229                         cmd:   "copy newdir2/testfile2 testfile3\n",
230                         match: `(?ms).*succeeded.*`,
231                 },
232                 {
233                         path:  writePath,
234                         cmd:   "get testfile3 '" + checkfile.Name() + "'\n",
235                         match: `(?ms).*succeeded.*`,
236                         data:  testdata,
237                 },
238                 {
239                         path:  writePath,
240                         cmd:   "get newdir2/testfile2 '" + checkfile.Name() + "'\n",
241                         match: `(?ms).*succeeded.*`,
242                         data:  testdata,
243                 },
244                 {
245                         path:  writePath,
246                         cmd:   "rmcol newdir2\n",
247                         match: `(?ms).*Deleting collection .* succeeded.*`,
248                 },
249                 {
250                         path:  writePath,
251                         cmd:   "get newdir2/testfile2 '" + checkfile.Name() + "'\n",
252                         match: `(?ms).*Downloading .* failed.*`,
253                 },
254                 {
255                         path:  "/c=" + arvadostest.UserAgreementCollection + "/t=" + arv.AuthToken + "/",
256                         cmd:   "put '" + localfile.Name() + "' foo\n",
257                         match: `(?ms).*Uploading .* failed:.*403 Forbidden.*`,
258                 },
259                 {
260                         path:  pdhPath,
261                         cmd:   "put '" + localfile.Name() + "' foo\n",
262                         match: `(?ms).*Uploading .* failed:.*405 Method Not Allowed.*`,
263                 },
264                 {
265                         path:  pdhPath,
266                         cmd:   "move foo bar\n",
267                         match: `(?ms).*Moving .* failed:.*405 Method Not Allowed.*`,
268                 },
269                 {
270                         path:  pdhPath,
271                         cmd:   "copy foo bar\n",
272                         match: `(?ms).*Copying .* failed:.*405 Method Not Allowed.*`,
273                 },
274                 {
275                         path:  pdhPath,
276                         cmd:   "delete foo\n",
277                         match: `(?ms).*Deleting .* failed:.*405 Method Not Allowed.*`,
278                 },
279                 {
280                         path:  pdhPath,
281                         cmd:   "lock foo\n",
282                         match: `(?ms).*Locking .* failed:.*405 Method Not Allowed.*`,
283                 },
284         } {
285                 c.Logf("%s %+v", s.testServer.URL, trial)
286                 if skip != nil && skip(trial.path) {
287                         c.Log("(skip)")
288                         continue
289                 }
290
291                 os.Remove(checkfile.Name())
292
293                 stdout := s.runCadaver(c, password, trial.path, trial.cmd)
294                 c.Check(stdout, check.Matches, trial.match)
295
296                 if trial.data == "" && !trial.checkemptydata {
297                         continue
298                 }
299                 checkfile, err = os.Open(checkfile.Name())
300                 c.Assert(err, check.IsNil)
301                 checkfile.Seek(0, os.SEEK_SET)
302                 got, err := ioutil.ReadAll(checkfile)
303                 c.Check(string(got), check.Equals, trial.data)
304                 c.Check(err, check.IsNil)
305         }
306 }
307
308 func (s *IntegrationSuite) TestCadaverByID(c *check.C) {
309         for _, path := range []string{"/by_id", "/by_id/"} {
310                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
311                 c.Check(stdout, check.Matches, `(?ms).*collection is empty.*`)
312         }
313         for _, path := range []string{
314                 "/by_id/" + arvadostest.FooCollectionPDH,
315                 "/by_id/" + arvadostest.FooCollectionPDH + "/",
316                 "/by_id/" + arvadostest.FooCollection,
317                 "/by_id/" + arvadostest.FooCollection + "/",
318         } {
319                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
320                 c.Check(stdout, check.Matches, `(?ms).*\s+foo\s+3 .*`)
321         }
322 }
323
324 func (s *IntegrationSuite) TestCadaverUsersDir(c *check.C) {
325         for _, path := range []string{"/"} {
326                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
327                 c.Check(stdout, check.Matches, `(?ms).*Coll:\s+by_id\s+0 .*`)
328                 c.Check(stdout, check.Matches, `(?ms).*Coll:\s+users\s+0 .*`)
329         }
330         for _, path := range []string{"/users", "/users/"} {
331                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
332                 c.Check(stdout, check.Matches, `(?ms).*Coll:\s+active.*`)
333         }
334         for _, path := range []string{"/users/active", "/users/active/"} {
335                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
336                 c.Check(stdout, check.Matches, `(?ms).*Coll:\s+A Project\s+0 .*`)
337                 c.Check(stdout, check.Matches, `(?ms).*Coll:\s+bar_file\s+0 .*`)
338         }
339         for _, path := range []string{"/users/admin", "/users/doesnotexist", "/users/doesnotexist/"} {
340                 stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
341                 c.Check(stdout, check.Matches, `(?ms).*404 Not Found.*`)
342         }
343 }
344
345 func (s *IntegrationSuite) runCadaver(c *check.C, password, path, stdin string) string {
346         tempdir, err := ioutil.TempDir("", "keep-web-test-")
347         c.Assert(err, check.IsNil)
348         defer os.RemoveAll(tempdir)
349
350         cmd := exec.Command("cadaver", s.testServer.URL+path)
351         if password != "" {
352                 // cadaver won't try username/password authentication
353                 // unless the server responds 401 to an
354                 // unauthenticated request, which it only does in
355                 // AttachmentOnlyHost, TrustAllContent, and
356                 // per-collection vhost cases.
357                 s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = s.testServer.URL[7:]
358
359                 cmd.Env = append(os.Environ(), "HOME="+tempdir)
360                 f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
361                 c.Assert(err, check.IsNil)
362                 _, err = fmt.Fprintf(f, "default login none password %s\n", password)
363                 c.Assert(err, check.IsNil)
364                 c.Assert(f.Close(), check.IsNil)
365         }
366         cmd.Stdin = bytes.NewBufferString(stdin)
367         stdout, err := cmd.StdoutPipe()
368         c.Assert(err, check.Equals, nil)
369         cmd.Stderr = cmd.Stdout
370         go cmd.Start()
371
372         var buf bytes.Buffer
373         _, err = io.Copy(&buf, stdout)
374         c.Check(err, check.Equals, nil)
375         err = cmd.Wait()
376         c.Check(err, check.Equals, nil)
377         return buf.String()
378 }