Merge branch '20667-maxsuper-atquota'
[arvados.git] / lib / controller / federation / user_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package federation
6
7 import (
8         "context"
9         "encoding/json"
10         "errors"
11         "math"
12         "net/url"
13         "os"
14         "strings"
15
16         "git.arvados.org/arvados.git/lib/controller/rpc"
17         "git.arvados.org/arvados.git/lib/ctrlctx"
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "git.arvados.org/arvados.git/sdk/go/arvadostest"
20         "git.arvados.org/arvados.git/sdk/go/auth"
21         "git.arvados.org/arvados.git/sdk/go/ctxlog"
22         check "gopkg.in/check.v1"
23 )
24
25 var _ = check.Suite(&UserSuite{})
26
27 type UserSuite struct {
28         FederationSuite
29 }
30
31 func (s *UserSuite) TestLoginClusterUserList(c *check.C) {
32         s.cluster.ClusterID = "local"
33         s.cluster.Login.LoginCluster = "zzzzz"
34         s.fed = New(s.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB)
35         s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")}, true, rpc.PassthroughTokenProvider))
36
37         for _, updateFail := range []bool{false, true} {
38                 for _, opts := range []arvados.ListOptions{
39                         {Offset: 0, Limit: -1, Select: nil},
40                         {Offset: 0, Limit: math.MaxInt64, Select: nil},
41                         {Offset: 1, Limit: 1, Select: nil},
42                         {Offset: 0, Limit: 2, Select: []string{"uuid"}},
43                         {Offset: 0, Limit: 2, Select: []string{"uuid", "email"}},
44                 } {
45                         c.Logf("updateFail %v, opts %#v", updateFail, opts)
46                         spy := arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
47                         stub := &arvadostest.APIStub{Error: errors.New("local cluster failure")}
48                         if updateFail {
49                                 s.fed.local = stub
50                         } else {
51                                 s.fed.local = rpc.NewConn(s.cluster.ClusterID, spy.URL, true, rpc.PassthroughTokenProvider)
52                         }
53                         userlist, err := s.fed.UserList(s.ctx, opts)
54                         if err != nil {
55                                 c.Logf("... UserList failed %q", err)
56                         }
57                         if updateFail && err == nil {
58                                 // All local updates fail, so the only
59                                 // cases expected to succeed are the
60                                 // ones with 0 results.
61                                 c.Check(userlist.Items, check.HasLen, 0)
62                                 c.Check(stub.Calls(nil), check.HasLen, 0)
63                         } else if updateFail {
64                                 c.Logf("... err %#v", err)
65                                 calls := stub.Calls(stub.UserBatchUpdate)
66                                 if c.Check(calls, check.HasLen, 1) {
67                                         c.Logf("... stub.UserUpdate called with options: %#v", calls[0].Options)
68                                         shouldUpdate := map[string]bool{
69                                                 "uuid":       false,
70                                                 "email":      true,
71                                                 "first_name": true,
72                                                 "last_name":  true,
73                                                 "is_admin":   true,
74                                                 "is_active":  true,
75                                                 "prefs":      true,
76                                                 // can't safely update locally
77                                                 "owner_uuid":   false,
78                                                 "identity_url": false,
79                                                 // virtual attrs
80                                                 "full_name":  false,
81                                                 "is_invited": false,
82                                         }
83                                         if opts.Select != nil {
84                                                 // Only the selected
85                                                 // fields (minus uuid)
86                                                 // should be updated.
87                                                 for k := range shouldUpdate {
88                                                         shouldUpdate[k] = false
89                                                 }
90                                                 for _, k := range opts.Select {
91                                                         if k != "uuid" {
92                                                                 shouldUpdate[k] = true
93                                                         }
94                                                 }
95                                         }
96                                         var uuid string
97                                         for uuid = range calls[0].Options.(arvados.UserBatchUpdateOptions).Updates {
98                                         }
99                                         for k, shouldFind := range shouldUpdate {
100                                                 _, found := calls[0].Options.(arvados.UserBatchUpdateOptions).Updates[uuid][k]
101                                                 c.Check(found, check.Equals, shouldFind, check.Commentf("offending attr: %s", k))
102                                         }
103                                 }
104                         } else {
105                                 updates := 0
106                                 for _, d := range spy.RequestDumps {
107                                         d := string(d)
108                                         if strings.Contains(d, "PATCH /arvados/v1/users/batch") {
109                                                 c.Check(d, check.Matches, `(?ms).*Authorization: Bearer `+arvadostest.SystemRootToken+`.*`)
110                                                 updates++
111                                         }
112                                 }
113                                 c.Check(err, check.IsNil)
114                                 c.Check(updates, check.Equals, 1)
115                                 c.Logf("... response items %#v", userlist.Items)
116                         }
117                 }
118         }
119 }
120
121 func (s *UserSuite) TestLoginClusterUserGet(c *check.C) {
122         s.cluster.ClusterID = "local"
123         s.cluster.Login.LoginCluster = "zzzzz"
124         s.fed = New(s.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB)
125         s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")}, true, rpc.PassthroughTokenProvider))
126
127         opts := arvados.GetOptions{UUID: "zzzzz-tpzed-xurymjxw79nv3jz", Select: []string{"uuid", "email"}}
128
129         stub := &arvadostest.APIStub{Error: errors.New("local cluster failure")}
130         s.fed.local = stub
131         s.fed.UserGet(s.ctx, opts)
132
133         calls := stub.Calls(stub.UserBatchUpdate)
134         if c.Check(calls, check.HasLen, 1) {
135                 c.Logf("... stub.UserUpdate called with options: %#v", calls[0].Options)
136                 shouldUpdate := map[string]bool{
137                         "uuid":       false,
138                         "email":      true,
139                         "first_name": true,
140                         "last_name":  true,
141                         "is_admin":   true,
142                         "is_active":  true,
143                         "prefs":      true,
144                         // can't safely update locally
145                         "owner_uuid":   false,
146                         "identity_url": false,
147                         // virtual attrs
148                         "full_name":  false,
149                         "is_invited": false,
150                 }
151                 if opts.Select != nil {
152                         // Only the selected
153                         // fields (minus uuid)
154                         // should be updated.
155                         for k := range shouldUpdate {
156                                 shouldUpdate[k] = false
157                         }
158                         for _, k := range opts.Select {
159                                 if k != "uuid" {
160                                         shouldUpdate[k] = true
161                                 }
162                         }
163                 }
164                 var uuid string
165                 for uuid = range calls[0].Options.(arvados.UserBatchUpdateOptions).Updates {
166                 }
167                 for k, shouldFind := range shouldUpdate {
168                         _, found := calls[0].Options.(arvados.UserBatchUpdateOptions).Updates[uuid][k]
169                         c.Check(found, check.Equals, shouldFind, check.Commentf("offending attr: %s", k))
170                 }
171         }
172
173 }
174
175 func (s *UserSuite) TestLoginClusterUserListBypassFederation(c *check.C) {
176         s.cluster.ClusterID = "local"
177         s.cluster.Login.LoginCluster = "zzzzz"
178         s.fed = New(s.ctx, s.cluster, nil, (&ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL}).GetDB)
179         s.addDirectRemote(c, "zzzzz", rpc.NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")},
180                 true, rpc.PassthroughTokenProvider))
181
182         spy := arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
183         s.fed.local = rpc.NewConn(s.cluster.ClusterID, spy.URL, true, rpc.PassthroughTokenProvider)
184
185         _, err := s.fed.UserList(s.ctx, arvados.ListOptions{Offset: 0, Limit: math.MaxInt64, Select: nil, BypassFederation: true})
186         // this will fail because it is not using a root token
187         c.Check(err.(*arvados.TransactionError).StatusCode, check.Equals, 403)
188
189         // Now use SystemRootToken
190         ctx := context.Background()
191         ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
192         ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.SystemRootToken}})
193
194         // Assert that it did not try to batch update users.
195         _, err = s.fed.UserList(ctx, arvados.ListOptions{Offset: 0, Limit: math.MaxInt64, Select: nil, BypassFederation: true})
196         for _, d := range spy.RequestDumps {
197                 d := string(d)
198                 if strings.Contains(d, "PATCH /arvados/v1/users/batch") {
199                         c.Fail()
200                 }
201         }
202         c.Check(err, check.IsNil)
203 }
204
205 // userAttrsCachedFromLoginCluster must have an entry for every field
206 // in the User struct.
207 func (s *UserSuite) TestUserAttrsUpdateWhitelist(c *check.C) {
208         buf, err := json.Marshal(&arvados.User{})
209         c.Assert(err, check.IsNil)
210         var allFields map[string]interface{}
211         err = json.Unmarshal(buf, &allFields)
212         c.Assert(err, check.IsNil)
213         for k := range allFields {
214                 _, ok := userAttrsCachedFromLoginCluster[k]
215                 c.Check(ok, check.Equals, true, check.Commentf("field name %q missing from userAttrsCachedFromLoginCluster", k))
216         }
217 }