Merge branch '17948-test-collection-tool' into main. Closes #17948
[arvados.git] / lib / controller / integration_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package controller
6
7 import (
8         "bytes"
9         "context"
10         "database/sql"
11         "encoding/json"
12         "fmt"
13         "io"
14         "io/ioutil"
15         "math"
16         "net"
17         "net/http"
18         "os"
19         "os/exec"
20         "path/filepath"
21         "strconv"
22         "strings"
23         "sync"
24
25         "git.arvados.org/arvados.git/lib/boot"
26         "git.arvados.org/arvados.git/lib/config"
27         "git.arvados.org/arvados.git/sdk/go/arvados"
28         "git.arvados.org/arvados.git/sdk/go/arvadostest"
29         "git.arvados.org/arvados.git/sdk/go/ctxlog"
30         "git.arvados.org/arvados.git/sdk/go/httpserver"
31         check "gopkg.in/check.v1"
32 )
33
34 var _ = check.Suite(&IntegrationSuite{})
35
36 type IntegrationSuite struct {
37         testClusters map[string]*boot.TestCluster
38         oidcprovider *arvadostest.OIDCProvider
39 }
40
41 func (s *IntegrationSuite) SetUpSuite(c *check.C) {
42         cwd, _ := os.Getwd()
43
44         s.oidcprovider = arvadostest.NewOIDCProvider(c)
45         s.oidcprovider.AuthEmail = "user@example.com"
46         s.oidcprovider.AuthEmailVerified = true
47         s.oidcprovider.AuthName = "Example User"
48         s.oidcprovider.ValidClientID = "clientid"
49         s.oidcprovider.ValidClientSecret = "clientsecret"
50
51         s.testClusters = map[string]*boot.TestCluster{
52                 "z1111": nil,
53                 "z2222": nil,
54                 "z3333": nil,
55         }
56         hostport := map[string]string{}
57         for id := range s.testClusters {
58                 hostport[id] = func() string {
59                         // TODO: Instead of expecting random ports on
60                         // 127.0.0.11, 22, 33 to be race-safe, try
61                         // different 127.x.y.z until finding one that
62                         // isn't in use.
63                         ln, err := net.Listen("tcp", ":0")
64                         c.Assert(err, check.IsNil)
65                         ln.Close()
66                         _, port, err := net.SplitHostPort(ln.Addr().String())
67                         c.Assert(err, check.IsNil)
68                         return "127.0.0." + id[3:] + ":" + port
69                 }()
70         }
71         for id := range s.testClusters {
72                 yaml := `Clusters:
73   ` + id + `:
74     Services:
75       Controller:
76         ExternalURL: https://` + hostport[id] + `
77     TLS:
78       Insecure: true
79     SystemLogs:
80       Format: text
81     RemoteClusters:
82       z1111:
83         Host: ` + hostport["z1111"] + `
84         Scheme: https
85         Insecure: true
86         Proxy: true
87         ActivateUsers: true
88 `
89                 if id != "z2222" {
90                         yaml += `      z2222:
91         Host: ` + hostport["z2222"] + `
92         Scheme: https
93         Insecure: true
94         Proxy: true
95         ActivateUsers: true
96 `
97                 }
98                 if id != "z3333" {
99                         yaml += `      z3333:
100         Host: ` + hostport["z3333"] + `
101         Scheme: https
102         Insecure: true
103         Proxy: true
104         ActivateUsers: true
105 `
106                 }
107                 if id == "z1111" {
108                         yaml += `
109     Login:
110       LoginCluster: z1111
111       OpenIDConnect:
112         Enable: true
113         Issuer: ` + s.oidcprovider.Issuer.URL + `
114         ClientID: ` + s.oidcprovider.ValidClientID + `
115         ClientSecret: ` + s.oidcprovider.ValidClientSecret + `
116         EmailClaim: email
117         EmailVerifiedClaim: email_verified
118         AcceptAccessToken: true
119         AcceptAccessTokenScope: ""
120 `
121                 } else {
122                         yaml += `
123     Login:
124       LoginCluster: z1111
125 `
126                 }
127
128                 loader := config.NewLoader(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
129                 loader.Path = "-"
130                 loader.SkipLegacy = true
131                 loader.SkipAPICalls = true
132                 cfg, err := loader.Load()
133                 c.Assert(err, check.IsNil)
134                 tc := boot.NewTestCluster(
135                         filepath.Join(cwd, "..", ".."),
136                         id, cfg, "127.0.0."+id[3:], c.Log)
137                 tc.Super.NoWorkbench1 = true
138                 tc.Start()
139                 s.testClusters[id] = tc
140         }
141         for _, tc := range s.testClusters {
142                 ok := tc.WaitReady()
143                 c.Assert(ok, check.Equals, true)
144         }
145 }
146
147 func (s *IntegrationSuite) TearDownSuite(c *check.C) {
148         for _, c := range s.testClusters {
149                 c.Super.Stop()
150         }
151 }
152
153 func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
154         conn1 := s.testClusters["z1111"].Conn()
155         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
156         conn3 := s.testClusters["z3333"].Conn()
157         userctx1, ac1, kc1, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
158
159         // Create the collection to find its PDH (but don't save it
160         // anywhere yet)
161         var coll1 arvados.Collection
162         fs1, err := coll1.FileSystem(ac1, kc1)
163         c.Assert(err, check.IsNil)
164         f, err := fs1.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
165         c.Assert(err, check.IsNil)
166         _, err = io.WriteString(f, "IntegrationSuite.TestGetCollectionByPDH")
167         c.Assert(err, check.IsNil)
168         err = f.Close()
169         c.Assert(err, check.IsNil)
170         mtxt, err := fs1.MarshalManifest(".")
171         c.Assert(err, check.IsNil)
172         pdh := arvados.PortableDataHash(mtxt)
173
174         // Looking up the PDH before saving returns 404 if cycle
175         // detection is working.
176         _, err = conn1.CollectionGet(userctx1, arvados.GetOptions{UUID: pdh})
177         c.Assert(err, check.ErrorMatches, `.*404 Not Found.*`)
178
179         // Save the collection on cluster z1111.
180         coll1, err = conn1.CollectionCreate(userctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
181                 "manifest_text": mtxt,
182         }})
183         c.Assert(err, check.IsNil)
184
185         // Retrieve the collection from cluster z3333.
186         coll, err := conn3.CollectionGet(userctx1, arvados.GetOptions{UUID: pdh})
187         c.Check(err, check.IsNil)
188         c.Check(coll.PortableDataHash, check.Equals, pdh)
189 }
190
191 // Tests bug #18004
192 func (s *IntegrationSuite) TestRemoteUserAndTokenCacheRace(c *check.C) {
193         conn1 := s.testClusters["z1111"].Conn()
194         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
195         rootctx2, _, _ := s.testClusters["z2222"].RootClients()
196         conn2 := s.testClusters["z2222"].Conn()
197         userctx1, _, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, "user2@example.com", true)
198
199         var wg1, wg2 sync.WaitGroup
200         creqs := 100
201
202         // Make concurrent requests to z2222 with a local token to make sure more
203         // than one worker is listening.
204         wg1.Add(1)
205         for i := 0; i < creqs; i++ {
206                 wg2.Add(1)
207                 go func() {
208                         defer wg2.Done()
209                         wg1.Wait()
210                         _, err := conn2.UserGetCurrent(rootctx2, arvados.GetOptions{})
211                         c.Check(err, check.IsNil, check.Commentf("warm up phase failed"))
212                 }()
213         }
214         wg1.Done()
215         wg2.Wait()
216
217         // Real test pass -- use a new remote token than the one used in the warm-up
218         // phase.
219         wg1.Add(1)
220         for i := 0; i < creqs; i++ {
221                 wg2.Add(1)
222                 go func() {
223                         defer wg2.Done()
224                         wg1.Wait()
225                         // Retrieve the remote collection from cluster z2222.
226                         _, err := conn2.UserGetCurrent(userctx1, arvados.GetOptions{})
227                         c.Check(err, check.IsNil, check.Commentf("testing phase failed"))
228                 }()
229         }
230         wg1.Done()
231         wg2.Wait()
232 }
233
234 func (s *IntegrationSuite) TestS3WithFederatedToken(c *check.C) {
235         if _, err := exec.LookPath("s3cmd"); err != nil {
236                 c.Skip("s3cmd not in PATH")
237                 return
238         }
239
240         testText := "IntegrationSuite.TestS3WithFederatedToken"
241
242         conn1 := s.testClusters["z1111"].Conn()
243         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
244         userctx1, ac1, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
245         conn3 := s.testClusters["z3333"].Conn()
246
247         createColl := func(clusterID string) arvados.Collection {
248                 _, ac, kc := s.testClusters[clusterID].ClientsWithToken(ac1.AuthToken)
249                 var coll arvados.Collection
250                 fs, err := coll.FileSystem(ac, kc)
251                 c.Assert(err, check.IsNil)
252                 f, err := fs.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
253                 c.Assert(err, check.IsNil)
254                 _, err = io.WriteString(f, testText)
255                 c.Assert(err, check.IsNil)
256                 err = f.Close()
257                 c.Assert(err, check.IsNil)
258                 mtxt, err := fs.MarshalManifest(".")
259                 c.Assert(err, check.IsNil)
260                 coll, err = s.testClusters[clusterID].Conn().CollectionCreate(userctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
261                         "manifest_text": mtxt,
262                 }})
263                 c.Assert(err, check.IsNil)
264                 return coll
265         }
266
267         for _, trial := range []struct {
268                 clusterID string // create the collection on this cluster (then use z3333 to access it)
269                 token     string
270         }{
271                 // Try the hardest test first: z3333 hasn't seen
272                 // z1111's token yet, and we're just passing the
273                 // opaque secret part, so z3333 has to guess that it
274                 // belongs to z1111.
275                 {"z1111", strings.Split(ac1.AuthToken, "/")[2]},
276                 {"z3333", strings.Split(ac1.AuthToken, "/")[2]},
277                 {"z1111", strings.Replace(ac1.AuthToken, "/", "_", -1)},
278                 {"z3333", strings.Replace(ac1.AuthToken, "/", "_", -1)},
279         } {
280                 c.Logf("================ %v", trial)
281                 coll := createColl(trial.clusterID)
282
283                 cfgjson, err := conn3.ConfigGet(userctx1)
284                 c.Assert(err, check.IsNil)
285                 var cluster arvados.Cluster
286                 err = json.Unmarshal(cfgjson, &cluster)
287                 c.Assert(err, check.IsNil)
288
289                 c.Logf("TokenV2 is %s", ac1.AuthToken)
290                 host := cluster.Services.WebDAV.ExternalURL.Host
291                 s3args := []string{
292                         "--ssl", "--no-check-certificate",
293                         "--host=" + host, "--host-bucket=" + host,
294                         "--access_key=" + trial.token, "--secret_key=" + trial.token,
295                 }
296                 buf, err := exec.Command("s3cmd", append(s3args, "ls", "s3://"+coll.UUID)...).CombinedOutput()
297                 c.Check(err, check.IsNil)
298                 c.Check(string(buf), check.Matches, `.* `+fmt.Sprintf("%d", len(testText))+` +s3://`+coll.UUID+`/test.txt\n`)
299
300                 buf, _ = exec.Command("s3cmd", append(s3args, "get", "s3://"+coll.UUID+"/test.txt", c.MkDir()+"/tmpfile")...).CombinedOutput()
301                 // Command fails because we don't return Etag header.
302                 flen := strconv.Itoa(len(testText))
303                 c.Check(string(buf), check.Matches, `(?ms).*`+flen+` (bytes in|of `+flen+`).*`)
304         }
305 }
306
307 func (s *IntegrationSuite) TestGetCollectionAsAnonymous(c *check.C) {
308         conn1 := s.testClusters["z1111"].Conn()
309         conn3 := s.testClusters["z3333"].Conn()
310         rootctx1, rootac1, rootkc1 := s.testClusters["z1111"].RootClients()
311         anonctx3, anonac3, _ := s.testClusters["z3333"].AnonymousClients()
312
313         // Make sure anonymous token was set
314         c.Assert(anonac3.AuthToken, check.Not(check.Equals), "")
315
316         // Create the collection to find its PDH (but don't save it
317         // anywhere yet)
318         var coll1 arvados.Collection
319         fs1, err := coll1.FileSystem(rootac1, rootkc1)
320         c.Assert(err, check.IsNil)
321         f, err := fs1.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
322         c.Assert(err, check.IsNil)
323         _, err = io.WriteString(f, "IntegrationSuite.TestGetCollectionAsAnonymous")
324         c.Assert(err, check.IsNil)
325         err = f.Close()
326         c.Assert(err, check.IsNil)
327         mtxt, err := fs1.MarshalManifest(".")
328         c.Assert(err, check.IsNil)
329         pdh := arvados.PortableDataHash(mtxt)
330
331         // Save the collection on cluster z1111.
332         coll1, err = conn1.CollectionCreate(rootctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
333                 "manifest_text": mtxt,
334         }})
335         c.Assert(err, check.IsNil)
336
337         // Share it with the anonymous users group.
338         var outLink arvados.Link
339         err = rootac1.RequestAndDecode(&outLink, "POST", "/arvados/v1/links", nil,
340                 map[string]interface{}{"link": map[string]interface{}{
341                         "link_class": "permission",
342                         "name":       "can_read",
343                         "tail_uuid":  "z1111-j7d0g-anonymouspublic",
344                         "head_uuid":  coll1.UUID,
345                 },
346                 })
347         c.Check(err, check.IsNil)
348
349         // Current user should be z3 anonymous user
350         outUser, err := anonac3.CurrentUser()
351         c.Check(err, check.IsNil)
352         c.Check(outUser.UUID, check.Equals, "z3333-tpzed-anonymouspublic")
353
354         // Get the token uuid
355         var outAuth arvados.APIClientAuthorization
356         err = anonac3.RequestAndDecode(&outAuth, "GET", "/arvados/v1/api_client_authorizations/current", nil, nil)
357         c.Check(err, check.IsNil)
358
359         // Make a v2 token of the z3 anonymous user, and use it on z1
360         _, anonac1, _ := s.testClusters["z1111"].ClientsWithToken(outAuth.TokenV2())
361         outUser2, err := anonac1.CurrentUser()
362         c.Check(err, check.IsNil)
363         // z3 anonymous user will be mapped to the z1 anonymous user
364         c.Check(outUser2.UUID, check.Equals, "z1111-tpzed-anonymouspublic")
365
366         // Retrieve the collection (which is on z1) using anonymous from cluster z3333.
367         coll, err := conn3.CollectionGet(anonctx3, arvados.GetOptions{UUID: coll1.UUID})
368         c.Check(err, check.IsNil)
369         c.Check(coll.PortableDataHash, check.Equals, pdh)
370 }
371
372 // Get a token from the login cluster (z1111), use it to submit a
373 // container request on z2222.
374 func (s *IntegrationSuite) TestCreateContainerRequestWithFedToken(c *check.C) {
375         conn1 := s.testClusters["z1111"].Conn()
376         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
377         _, ac1, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
378
379         // Use ac2 to get the discovery doc with a blank token, so the
380         // SDK doesn't magically pass the z1111 token to z2222 before
381         // we're ready to start our test.
382         _, ac2, _ := s.testClusters["z2222"].ClientsWithToken("")
383         var dd map[string]interface{}
384         err := ac2.RequestAndDecode(&dd, "GET", "discovery/v1/apis/arvados/v1/rest", nil, nil)
385         c.Assert(err, check.IsNil)
386
387         var (
388                 body bytes.Buffer
389                 req  *http.Request
390                 resp *http.Response
391                 u    arvados.User
392                 cr   arvados.ContainerRequest
393         )
394         json.NewEncoder(&body).Encode(map[string]interface{}{
395                 "container_request": map[string]interface{}{
396                         "command":         []string{"echo"},
397                         "container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
398                         "cwd":             "/",
399                         "output_path":     "/",
400                 },
401         })
402         ac2.AuthToken = ac1.AuthToken
403
404         c.Log("...post CR with good (but not yet cached) token")
405         cr = arvados.ContainerRequest{}
406         req, err = http.NewRequest("POST", "https://"+ac2.APIHost+"/arvados/v1/container_requests", bytes.NewReader(body.Bytes()))
407         c.Assert(err, check.IsNil)
408         req.Header.Set("Content-Type", "application/json")
409         err = ac2.DoAndDecode(&cr, req)
410         c.Assert(err, check.IsNil)
411         c.Logf("err == %#v", err)
412
413         c.Log("...get user with good token")
414         u = arvados.User{}
415         req, err = http.NewRequest("GET", "https://"+ac2.APIHost+"/arvados/v1/users/current", nil)
416         c.Assert(err, check.IsNil)
417         err = ac2.DoAndDecode(&u, req)
418         c.Check(err, check.IsNil)
419         c.Check(u.UUID, check.Matches, "z1111-tpzed-.*")
420
421         c.Log("...post CR with good cached token")
422         cr = arvados.ContainerRequest{}
423         req, err = http.NewRequest("POST", "https://"+ac2.APIHost+"/arvados/v1/container_requests", bytes.NewReader(body.Bytes()))
424         c.Assert(err, check.IsNil)
425         req.Header.Set("Content-Type", "application/json")
426         err = ac2.DoAndDecode(&cr, req)
427         c.Check(err, check.IsNil)
428         c.Check(cr.UUID, check.Matches, "z2222-.*")
429
430         c.Log("...post with good cached token ('OAuth2 ...')")
431         cr = arvados.ContainerRequest{}
432         req, err = http.NewRequest("POST", "https://"+ac2.APIHost+"/arvados/v1/container_requests", bytes.NewReader(body.Bytes()))
433         c.Assert(err, check.IsNil)
434         req.Header.Set("Content-Type", "application/json")
435         req.Header.Set("Authorization", "OAuth2 "+ac2.AuthToken)
436         resp, err = arvados.InsecureHTTPClient.Do(req)
437         c.Assert(err, check.IsNil)
438         err = json.NewDecoder(resp.Body).Decode(&cr)
439         c.Check(err, check.IsNil)
440         c.Check(cr.UUID, check.Matches, "z2222-.*")
441 }
442
443 func (s *IntegrationSuite) TestCreateContainerRequestWithBadToken(c *check.C) {
444         conn1 := s.testClusters["z1111"].Conn()
445         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
446         _, ac1, _, au := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, "user@example.com", true)
447
448         tests := []struct {
449                 name         string
450                 token        string
451                 expectedCode int
452         }{
453                 {"Good token", ac1.AuthToken, http.StatusOK},
454                 {"Bogus token", "abcdef", http.StatusUnauthorized},
455                 {"v1-looking token", "badtoken00badtoken00badtoken00badtoken00b", http.StatusUnauthorized},
456                 {"v2-looking token", "v2/" + au.UUID + "/badtoken00badtoken00badtoken00badtoken00b", http.StatusUnauthorized},
457         }
458
459         body, _ := json.Marshal(map[string]interface{}{
460                 "container_request": map[string]interface{}{
461                         "command":         []string{"echo"},
462                         "container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
463                         "cwd":             "/",
464                         "output_path":     "/",
465                 },
466         })
467
468         for _, tt := range tests {
469                 c.Log(c.TestName() + " " + tt.name)
470                 ac1.AuthToken = tt.token
471                 req, err := http.NewRequest("POST", "https://"+ac1.APIHost+"/arvados/v1/container_requests", bytes.NewReader(body))
472                 c.Assert(err, check.IsNil)
473                 req.Header.Set("Content-Type", "application/json")
474                 resp, err := ac1.Do(req)
475                 c.Assert(err, check.IsNil)
476                 c.Assert(resp.StatusCode, check.Equals, tt.expectedCode)
477         }
478 }
479
480 func (s *IntegrationSuite) TestRequestIDHeader(c *check.C) {
481         conn1 := s.testClusters["z1111"].Conn()
482         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
483         userctx1, ac1, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, "user@example.com", true)
484
485         coll, err := conn1.CollectionCreate(userctx1, arvados.CreateOptions{})
486         c.Check(err, check.IsNil)
487         specimen, err := conn1.SpecimenCreate(userctx1, arvados.CreateOptions{})
488         c.Check(err, check.IsNil)
489
490         tests := []struct {
491                 path            string
492                 reqIdProvided   bool
493                 notFoundRequest bool
494         }{
495                 {"/arvados/v1/collections", false, false},
496                 {"/arvados/v1/collections", true, false},
497                 {"/arvados/v1/nonexistant", false, true},
498                 {"/arvados/v1/nonexistant", true, true},
499                 {"/arvados/v1/collections/" + coll.UUID, false, false},
500                 {"/arvados/v1/collections/" + coll.UUID, true, false},
501                 {"/arvados/v1/specimens/" + specimen.UUID, false, false},
502                 {"/arvados/v1/specimens/" + specimen.UUID, true, false},
503                 {"/arvados/v1/collections/z1111-4zz18-0123456789abcde", false, true},
504                 {"/arvados/v1/collections/z1111-4zz18-0123456789abcde", true, true},
505                 {"/arvados/v1/specimens/z1111-j58dm-0123456789abcde", false, true},
506                 {"/arvados/v1/specimens/z1111-j58dm-0123456789abcde", true, true},
507         }
508
509         for _, tt := range tests {
510                 c.Log(c.TestName() + " " + tt.path)
511                 req, err := http.NewRequest("GET", "https://"+ac1.APIHost+tt.path, nil)
512                 c.Assert(err, check.IsNil)
513                 customReqId := "abcdeG"
514                 if !tt.reqIdProvided {
515                         c.Assert(req.Header.Get("X-Request-Id"), check.Equals, "")
516                 } else {
517                         req.Header.Set("X-Request-Id", customReqId)
518                 }
519                 resp, err := ac1.Do(req)
520                 c.Assert(err, check.IsNil)
521                 if tt.notFoundRequest {
522                         c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
523                 } else {
524                         c.Check(resp.StatusCode, check.Equals, http.StatusOK)
525                 }
526                 if !tt.reqIdProvided {
527                         c.Check(resp.Header.Get("X-Request-Id"), check.Matches, "^req-[0-9a-zA-Z]{20}$")
528                         if tt.notFoundRequest {
529                                 var jresp httpserver.ErrorResponse
530                                 err := json.NewDecoder(resp.Body).Decode(&jresp)
531                                 c.Check(err, check.IsNil)
532                                 c.Assert(jresp.Errors, check.HasLen, 1)
533                                 c.Check(jresp.Errors[0], check.Matches, "^.*(req-[0-9a-zA-Z]{20}).*$")
534                         }
535                 } else {
536                         c.Check(resp.Header.Get("X-Request-Id"), check.Equals, customReqId)
537                         if tt.notFoundRequest {
538                                 var jresp httpserver.ErrorResponse
539                                 err := json.NewDecoder(resp.Body).Decode(&jresp)
540                                 c.Check(err, check.IsNil)
541                                 c.Assert(jresp.Errors, check.HasLen, 1)
542                                 c.Check(jresp.Errors[0], check.Matches, "^.*("+customReqId+").*$")
543                         }
544                 }
545         }
546 }
547
548 // We test the direct access to the database
549 // normally an integration test would not have a database access, but in this case we need
550 // to test tokens that are secret, so there is no API response that will give them back
551 func (s *IntegrationSuite) dbConn(c *check.C, clusterID string) (*sql.DB, *sql.Conn) {
552         ctx := context.Background()
553         db, err := sql.Open("postgres", s.testClusters[clusterID].Super.Cluster().PostgreSQL.Connection.String())
554         c.Assert(err, check.IsNil)
555
556         conn, err := db.Conn(ctx)
557         c.Assert(err, check.IsNil)
558
559         rows, err := conn.ExecContext(ctx, `SELECT 1`)
560         c.Assert(err, check.IsNil)
561         n, err := rows.RowsAffected()
562         c.Assert(err, check.IsNil)
563         c.Assert(n, check.Equals, int64(1))
564         return db, conn
565 }
566
567 // TestRuntimeTokenInCR will test several different tokens in the runtime attribute
568 // and check the expected results accessing directly to the database if needed.
569 func (s *IntegrationSuite) TestRuntimeTokenInCR(c *check.C) {
570         db, dbconn := s.dbConn(c, "z1111")
571         defer db.Close()
572         defer dbconn.Close()
573         conn1 := s.testClusters["z1111"].Conn()
574         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
575         userctx1, ac1, _, au := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, "user@example.com", true)
576
577         tests := []struct {
578                 name                 string
579                 token                string
580                 expectAToGetAValidCR bool
581                 expectedToken        *string
582         }{
583                 {"Good token z1111 user", ac1.AuthToken, true, &ac1.AuthToken},
584                 {"Bogus token", "abcdef", false, nil},
585                 {"v1-looking token", "badtoken00badtoken00badtoken00badtoken00b", false, nil},
586                 {"v2-looking token", "v2/" + au.UUID + "/badtoken00badtoken00badtoken00badtoken00b", false, nil},
587         }
588
589         for _, tt := range tests {
590                 c.Log(c.TestName() + " " + tt.name)
591
592                 rq := map[string]interface{}{
593                         "command":         []string{"echo"},
594                         "container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
595                         "cwd":             "/",
596                         "output_path":     "/",
597                         "runtime_token":   tt.token,
598                 }
599                 cr, err := conn1.ContainerRequestCreate(userctx1, arvados.CreateOptions{Attrs: rq})
600                 if tt.expectAToGetAValidCR {
601                         c.Check(err, check.IsNil)
602                         c.Check(cr, check.NotNil)
603                         c.Check(cr.UUID, check.Not(check.Equals), "")
604                 }
605
606                 if tt.expectedToken == nil {
607                         continue
608                 }
609
610                 c.Logf("cr.UUID: %s", cr.UUID)
611                 row := dbconn.QueryRowContext(rootctx1, `SELECT runtime_token from container_requests where uuid=$1`, cr.UUID)
612                 c.Check(row, check.NotNil)
613                 var token sql.NullString
614                 row.Scan(&token)
615                 if c.Check(token.Valid, check.Equals, true) {
616                         c.Check(token.String, check.Equals, *tt.expectedToken)
617                 }
618         }
619 }
620
621 // TestIntermediateCluster will send a container request to
622 // one cluster with another cluster as the destination
623 // and check the tokens are being handled properly
624 func (s *IntegrationSuite) TestIntermediateCluster(c *check.C) {
625         conn1 := s.testClusters["z1111"].Conn()
626         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
627         uctx1, ac1, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, "user@example.com", true)
628
629         tests := []struct {
630                 name                 string
631                 token                string
632                 expectedRuntimeToken string
633                 expectedUUIDprefix   string
634         }{
635                 {"Good token z1111 user sending a CR to z2222", ac1.AuthToken, "", "z2222-xvhdp-"},
636         }
637
638         for _, tt := range tests {
639                 c.Log(c.TestName() + " " + tt.name)
640                 rq := map[string]interface{}{
641                         "command":         []string{"echo"},
642                         "container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
643                         "cwd":             "/",
644                         "output_path":     "/",
645                         "runtime_token":   tt.token,
646                 }
647                 cr, err := conn1.ContainerRequestCreate(uctx1, arvados.CreateOptions{ClusterID: "z2222", Attrs: rq})
648
649                 c.Check(err, check.IsNil)
650                 c.Check(strings.HasPrefix(cr.UUID, tt.expectedUUIDprefix), check.Equals, true)
651                 c.Check(cr.RuntimeToken, check.Equals, tt.expectedRuntimeToken)
652         }
653 }
654
655 // Test for bug #16263
656 func (s *IntegrationSuite) TestListUsers(c *check.C) {
657         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
658         conn1 := s.testClusters["z1111"].Conn()
659         conn3 := s.testClusters["z3333"].Conn()
660         userctx1, _, _, _ := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
661
662         // Make sure LoginCluster is properly configured
663         for cls := range s.testClusters {
664                 c.Check(
665                         s.testClusters[cls].Config.Clusters[cls].Login.LoginCluster,
666                         check.Equals, "z1111",
667                         check.Commentf("incorrect LoginCluster config on cluster %q", cls))
668         }
669         // Make sure z1111 has users with NULL usernames
670         lst, err := conn1.UserList(rootctx1, arvados.ListOptions{
671                 Limit: math.MaxInt64, // check that large limit works (see #16263)
672         })
673         nullUsername := false
674         c.Assert(err, check.IsNil)
675         c.Assert(len(lst.Items), check.Not(check.Equals), 0)
676         for _, user := range lst.Items {
677                 if user.Username == "" {
678                         nullUsername = true
679                 }
680         }
681         c.Assert(nullUsername, check.Equals, true)
682
683         user1, err := conn1.UserGetCurrent(userctx1, arvados.GetOptions{})
684         c.Assert(err, check.IsNil)
685         c.Check(user1.IsActive, check.Equals, true)
686
687         // Ask for the user list on z3333 using z1111's system root token
688         lst, err = conn3.UserList(rootctx1, arvados.ListOptions{Limit: -1})
689         c.Assert(err, check.IsNil)
690         found := false
691         for _, user := range lst.Items {
692                 if user.UUID == user1.UUID {
693                         c.Check(user.IsActive, check.Equals, true)
694                         found = true
695                         break
696                 }
697         }
698         c.Check(found, check.Equals, true)
699
700         // Deactivate user acct on z1111
701         _, err = conn1.UserUnsetup(rootctx1, arvados.GetOptions{UUID: user1.UUID})
702         c.Assert(err, check.IsNil)
703
704         // Get user list from z3333, check the returned z1111 user is
705         // deactivated
706         lst, err = conn3.UserList(rootctx1, arvados.ListOptions{Limit: -1})
707         c.Assert(err, check.IsNil)
708         found = false
709         for _, user := range lst.Items {
710                 if user.UUID == user1.UUID {
711                         c.Check(user.IsActive, check.Equals, false)
712                         found = true
713                         break
714                 }
715         }
716         c.Check(found, check.Equals, true)
717
718         // Deactivated user can see is_active==false via "get current
719         // user" API
720         user1, err = conn3.UserGetCurrent(userctx1, arvados.GetOptions{})
721         c.Assert(err, check.IsNil)
722         c.Check(user1.IsActive, check.Equals, false)
723 }
724
725 func (s *IntegrationSuite) TestSetupUserWithVM(c *check.C) {
726         conn1 := s.testClusters["z1111"].Conn()
727         conn3 := s.testClusters["z3333"].Conn()
728         rootctx1, rootac1, _ := s.testClusters["z1111"].RootClients()
729
730         // Create user on LoginCluster z1111
731         _, _, _, user := s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
732
733         // Make a new root token (because rootClients() uses SystemRootToken)
734         var outAuth arvados.APIClientAuthorization
735         err := rootac1.RequestAndDecode(&outAuth, "POST", "/arvados/v1/api_client_authorizations", nil, nil)
736         c.Check(err, check.IsNil)
737
738         // Make a v2 root token to communicate with z3333
739         rootctx3, rootac3, _ := s.testClusters["z3333"].ClientsWithToken(outAuth.TokenV2())
740
741         // Create VM on z3333
742         var outVM arvados.VirtualMachine
743         err = rootac3.RequestAndDecode(&outVM, "POST", "/arvados/v1/virtual_machines", nil,
744                 map[string]interface{}{"virtual_machine": map[string]interface{}{
745                         "hostname": "example",
746                 },
747                 })
748         c.Check(outVM.UUID[0:5], check.Equals, "z3333")
749         c.Check(err, check.IsNil)
750
751         // Make sure z3333 user list is up to date
752         _, err = conn3.UserList(rootctx3, arvados.ListOptions{Limit: 1000})
753         c.Check(err, check.IsNil)
754
755         // Try to set up user on z3333 with the VM
756         _, err = conn3.UserSetup(rootctx3, arvados.UserSetupOptions{UUID: user.UUID, VMUUID: outVM.UUID})
757         c.Check(err, check.IsNil)
758
759         var outLinks arvados.LinkList
760         err = rootac3.RequestAndDecode(&outLinks, "GET", "/arvados/v1/links", nil,
761                 arvados.ListOptions{
762                         Limit: 1000,
763                         Filters: []arvados.Filter{
764                                 {
765                                         Attr:     "tail_uuid",
766                                         Operator: "=",
767                                         Operand:  user.UUID,
768                                 },
769                                 {
770                                         Attr:     "head_uuid",
771                                         Operator: "=",
772                                         Operand:  outVM.UUID,
773                                 },
774                                 {
775                                         Attr:     "name",
776                                         Operator: "=",
777                                         Operand:  "can_login",
778                                 },
779                                 {
780                                         Attr:     "link_class",
781                                         Operator: "=",
782                                         Operand:  "permission",
783                                 }}})
784         c.Check(err, check.IsNil)
785
786         c.Check(len(outLinks.Items), check.Equals, 1)
787 }
788
789 func (s *IntegrationSuite) TestOIDCAccessTokenAuth(c *check.C) {
790         conn1 := s.testClusters["z1111"].Conn()
791         rootctx1, _, _ := s.testClusters["z1111"].RootClients()
792         s.testClusters["z1111"].UserClients(rootctx1, c, conn1, s.oidcprovider.AuthEmail, true)
793
794         accesstoken := s.oidcprovider.ValidAccessToken()
795
796         for _, clusterID := range []string{"z1111", "z2222"} {
797
798                 var coll arvados.Collection
799
800                 // Write some file data and create a collection
801                 {
802                         c.Logf("save collection to %s", clusterID)
803
804                         conn := s.testClusters[clusterID].Conn()
805                         ctx, ac, kc := s.testClusters[clusterID].ClientsWithToken(accesstoken)
806
807                         fs, err := coll.FileSystem(ac, kc)
808                         c.Assert(err, check.IsNil)
809                         f, err := fs.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
810                         c.Assert(err, check.IsNil)
811                         _, err = io.WriteString(f, "IntegrationSuite.TestOIDCAccessTokenAuth")
812                         c.Assert(err, check.IsNil)
813                         err = f.Close()
814                         c.Assert(err, check.IsNil)
815                         mtxt, err := fs.MarshalManifest(".")
816                         c.Assert(err, check.IsNil)
817                         coll, err = conn.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
818                                 "manifest_text": mtxt,
819                         }})
820                         c.Assert(err, check.IsNil)
821                 }
822
823                 // Read the collection & file data -- both from the
824                 // cluster where it was created, and from the other
825                 // cluster.
826                 for _, readClusterID := range []string{"z1111", "z2222", "z3333"} {
827                         c.Logf("retrieve %s from %s", coll.UUID, readClusterID)
828
829                         conn := s.testClusters[readClusterID].Conn()
830                         ctx, ac, kc := s.testClusters[readClusterID].ClientsWithToken(accesstoken)
831
832                         user, err := conn.UserGetCurrent(ctx, arvados.GetOptions{})
833                         c.Assert(err, check.IsNil)
834                         c.Check(user.FullName, check.Equals, "Example User")
835                         readcoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: coll.UUID})
836                         c.Assert(err, check.IsNil)
837                         c.Check(readcoll.ManifestText, check.Not(check.Equals), "")
838                         fs, err := readcoll.FileSystem(ac, kc)
839                         c.Assert(err, check.IsNil)
840                         f, err := fs.Open("test.txt")
841                         c.Assert(err, check.IsNil)
842                         buf, err := ioutil.ReadAll(f)
843                         c.Assert(err, check.IsNil)
844                         c.Check(buf, check.DeepEquals, []byte("IntegrationSuite.TestOIDCAccessTokenAuth"))
845                 }
846         }
847 }