16522: Merge branch 'master' into 16522-python3-arvados-fuse
[arvados.git] / tools / sync-groups / sync-groups_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "fmt"
9         "io/ioutil"
10         "os"
11         "strings"
12         "testing"
13
14         "git.arvados.org/arvados.git/sdk/go/arvados"
15         "git.arvados.org/arvados.git/sdk/go/arvadostest"
16         . "gopkg.in/check.v1"
17 )
18
19 // Gocheck boilerplate
20 func Test(t *testing.T) {
21         TestingT(t)
22 }
23
24 type TestSuite struct {
25         cfg   *ConfigParams
26         users map[string]arvados.User
27 }
28
29 func (s *TestSuite) SetUpSuite(c *C) {
30         arvadostest.StartAPI()
31 }
32
33 func (s *TestSuite) TearDownSuite(c *C) {
34         arvadostest.StopAPI()
35 }
36
37 func (s *TestSuite) SetUpTest(c *C) {
38         ac := arvados.NewClientFromEnv()
39         u, err := ac.CurrentUser()
40         c.Assert(err, IsNil)
41         // Check that the parent group doesn't exist
42         sysUserUUID := u.UUID[:12] + "000000000000000"
43         gl := arvados.GroupList{}
44         params := arvados.ResourceListParams{
45                 Filters: []arvados.Filter{{
46                         Attr:     "owner_uuid",
47                         Operator: "=",
48                         Operand:  sysUserUUID,
49                 }, {
50                         Attr:     "name",
51                         Operator: "=",
52                         Operand:  "Externally synchronized groups",
53                 }},
54         }
55         ac.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params)
56         c.Assert(gl.ItemsAvailable, Equals, 0)
57         // Set up config
58         os.Args = []string{"cmd", "somefile.csv"}
59         config, err := GetConfig()
60         c.Assert(err, IsNil)
61         // Confirm that the parent group was created
62         gl = arvados.GroupList{}
63         ac.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params)
64         c.Assert(gl.ItemsAvailable, Equals, 1)
65         // Config set up complete, save config for further testing
66         s.cfg = &config
67
68         // Fetch current user list
69         ul := arvados.UserList{}
70         params = arvados.ResourceListParams{
71                 Filters: []arvados.Filter{{
72                         Attr:     "uuid",
73                         Operator: "!=",
74                         Operand:  s.cfg.SysUserUUID,
75                 }},
76         }
77         ac.RequestAndDecode(&ul, "GET", "/arvados/v1/users", nil, params)
78         c.Assert(ul.ItemsAvailable, Not(Equals), 0)
79         s.users = make(map[string]arvados.User)
80         for _, u := range ul.Items {
81                 s.users[u.UUID] = u
82         }
83         c.Assert(len(s.users), Not(Equals), 0)
84 }
85
86 func (s *TestSuite) TearDownTest(c *C) {
87         var dst interface{}
88         // Reset database to fixture state after every test run.
89         err := s.cfg.Client.RequestAndDecode(&dst, "POST", "/database/reset", nil, nil)
90         c.Assert(err, IsNil)
91 }
92
93 var _ = Suite(&TestSuite{})
94
95 // MakeTempCSVFile creates a temp file with data as comma separated values
96 func MakeTempCSVFile(data [][]string) (f *os.File, err error) {
97         f, err = ioutil.TempFile("", "test_sync_remote_groups")
98         if err != nil {
99                 return
100         }
101         for _, line := range data {
102                 fmt.Fprintf(f, "%s\n", strings.Join(line, ","))
103         }
104         err = f.Close()
105         return
106 }
107
108 // GroupMembershipExists checks that both needed links exist between user and group
109 func GroupMembershipExists(ac *arvados.Client, userUUID string, groupUUID string, perm string) bool {
110         ll := LinkList{}
111         // Check Group -> User can_read permission
112         params := arvados.ResourceListParams{
113                 Filters: []arvados.Filter{{
114                         Attr:     "link_class",
115                         Operator: "=",
116                         Operand:  "permission",
117                 }, {
118                         Attr:     "tail_uuid",
119                         Operator: "=",
120                         Operand:  groupUUID,
121                 }, {
122                         Attr:     "name",
123                         Operator: "=",
124                         Operand:  "can_read",
125                 }, {
126                         Attr:     "head_uuid",
127                         Operator: "=",
128                         Operand:  userUUID,
129                 }},
130         }
131         ac.RequestAndDecode(&ll, "GET", "/arvados/v1/links", nil, params)
132         if ll.Len() != 1 {
133                 return false
134         }
135         // Check User -> Group can_write permission
136         params = arvados.ResourceListParams{
137                 Filters: []arvados.Filter{{
138                         Attr:     "link_class",
139                         Operator: "=",
140                         Operand:  "permission",
141                 }, {
142                         Attr:     "head_uuid",
143                         Operator: "=",
144                         Operand:  groupUUID,
145                 }, {
146                         Attr:     "name",
147                         Operator: "=",
148                         Operand:  perm,
149                 }, {
150                         Attr:     "tail_uuid",
151                         Operator: "=",
152                         Operand:  userUUID,
153                 }},
154         }
155         ac.RequestAndDecode(&ll, "GET", "/arvados/v1/links", nil, params)
156         if ll.Len() != 1 {
157                 return false
158         }
159         return true
160 }
161
162 // If named group exists, return its UUID
163 func RemoteGroupExists(cfg *ConfigParams, groupName string) (uuid string, err error) {
164         gl := arvados.GroupList{}
165         params := arvados.ResourceListParams{
166                 Filters: []arvados.Filter{{
167                         Attr:     "name",
168                         Operator: "=",
169                         Operand:  groupName,
170                 }, {
171                         Attr:     "owner_uuid",
172                         Operator: "=",
173                         Operand:  cfg.SysUserUUID,
174                 }, {
175                         Attr:     "group_class",
176                         Operator: "=",
177                         Operand:  "role",
178                 }},
179         }
180         err = cfg.Client.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params)
181         if err != nil {
182                 return "", err
183         }
184         if gl.ItemsAvailable == 0 {
185                 // No group with this name
186                 uuid = ""
187         } else if gl.ItemsAvailable == 1 {
188                 // Group found
189                 uuid = gl.Items[0].UUID
190         } else {
191                 // This should never happen
192                 uuid = ""
193                 err = fmt.Errorf("more than 1 group found with the same name and parent")
194         }
195         return
196 }
197
198 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
199         cfg := ConfigParams{}
200         os.Args = []string{"cmd", "-verbose", "/tmp/somefile.csv"}
201         err := ParseFlags(&cfg)
202         c.Assert(err, IsNil)
203         c.Check(cfg.Path, Equals, "/tmp/somefile.csv")
204         c.Check(cfg.Verbose, Equals, true)
205 }
206
207 func (s *TestSuite) TestParseFlagsWithoutPositionalArgument(c *C) {
208         os.Args = []string{"cmd", "-verbose"}
209         err := ParseFlags(&ConfigParams{})
210         c.Assert(err, NotNil)
211 }
212
213 func (s *TestSuite) TestGetUserID(c *C) {
214         u := arvados.User{
215                 Email:    "testuser@example.com",
216                 Username: "Testuser",
217         }
218         email, err := GetUserID(u, "email")
219         c.Assert(err, IsNil)
220         c.Check(email, Equals, "testuser@example.com")
221         _, err = GetUserID(u, "bogus")
222         c.Assert(err, NotNil)
223 }
224
225 func (s *TestSuite) TestGetConfig(c *C) {
226         os.Args = []string{"cmd", "/tmp/somefile.csv"}
227         cfg, err := GetConfig()
228         c.Assert(err, IsNil)
229         c.Check(cfg.SysUserUUID, NotNil)
230         c.Check(cfg.Client, NotNil)
231         c.Check(cfg.ParentGroupUUID, NotNil)
232         c.Check(cfg.ParentGroupName, Equals, "Externally synchronized groups")
233 }
234
235 // Ignore leading & trailing spaces on group & users names
236 func (s *TestSuite) TestIgnoreSpaces(c *C) {
237         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
238         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
239         // Confirm that the groups don't exist
240         for _, groupName := range []string{"TestGroup1", "TestGroup2", "Test Group 3"} {
241                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
242                 c.Assert(err, IsNil)
243                 c.Assert(groupUUID, Equals, "")
244         }
245         data := [][]string{
246                 {" TestGroup1", activeUserEmail},
247                 {"TestGroup2 ", " " + activeUserEmail},
248                 {" Test Group 3 ", activeUserEmail + " "},
249         }
250         tmpfile, err := MakeTempCSVFile(data)
251         c.Assert(err, IsNil)
252         defer os.Remove(tmpfile.Name()) // clean up
253         s.cfg.Path = tmpfile.Name()
254         err = doMain(s.cfg)
255         c.Assert(err, IsNil)
256         // Check that 3 groups were created correctly, and have the active user as
257         // a member.
258         for _, groupName := range []string{"TestGroup1", "TestGroup2", "Test Group 3"} {
259                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
260                 c.Assert(err, IsNil)
261                 c.Assert(groupUUID, Not(Equals), "")
262                 c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID, "can_write"), Equals, true)
263         }
264 }
265
266 // Error out when records have <2 or >3 records
267 func (s *TestSuite) TestWrongNumberOfFields(c *C) {
268         for _, testCase := range [][][]string{
269                 {{"field1"}},
270                 {{"field1", "field2", "field3", "field4"}},
271                 {{"field1", "field2", "field3", "field4", "field5"}},
272         } {
273                 tmpfile, err := MakeTempCSVFile(testCase)
274                 c.Assert(err, IsNil)
275                 defer os.Remove(tmpfile.Name())
276                 s.cfg.Path = tmpfile.Name()
277                 err = doMain(s.cfg)
278                 c.Assert(err, Not(IsNil))
279         }
280 }
281
282 // Check different membership permissions
283 func (s *TestSuite) TestMembershipLevels(c *C) {
284         userEmail := s.users[arvadostest.ActiveUserUUID].Email
285         userUUID := s.users[arvadostest.ActiveUserUUID].UUID
286         data := [][]string{
287                 {"TestGroup1", userEmail, "can_read"},
288                 {"TestGroup2", userEmail, "can_write"},
289                 {"TestGroup3", userEmail, "can_manage"},
290                 {"TestGroup4", userEmail, "invalid_permission"},
291         }
292         tmpfile, err := MakeTempCSVFile(data)
293         c.Assert(err, IsNil)
294         defer os.Remove(tmpfile.Name()) // clean up
295         s.cfg.Path = tmpfile.Name()
296         err = doMain(s.cfg)
297         c.Assert(err, IsNil)
298         for _, record := range data {
299                 groupName := record[0]
300                 permLevel := record[2]
301                 if permLevel != "invalid_permission" {
302                         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
303                         c.Assert(err, IsNil)
304                         c.Assert(groupUUID, Not(Equals), "")
305                         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, permLevel), Equals, true)
306                 } else {
307                         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
308                         c.Assert(err, IsNil)
309                         c.Assert(groupUUID, Equals, "")
310                 }
311         }
312 }
313
314 // Check membership level change
315 func (s *TestSuite) TestMembershipLevelUpdate(c *C) {
316         userEmail := s.users[arvadostest.ActiveUserUUID].Email
317         userUUID := s.users[arvadostest.ActiveUserUUID].UUID
318         groupName := "TestGroup1"
319         // Give read permissions
320         tmpfile, err := MakeTempCSVFile([][]string{{groupName, userEmail, "can_read"}})
321         c.Assert(err, IsNil)
322         defer os.Remove(tmpfile.Name()) // clean up
323         s.cfg.Path = tmpfile.Name()
324         err = doMain(s.cfg)
325         c.Assert(err, IsNil)
326         // Check permissions
327         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
328         c.Assert(err, IsNil)
329         c.Assert(groupUUID, Not(Equals), "")
330         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_read"), Equals, true)
331         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_write"), Equals, false)
332         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_manage"), Equals, false)
333
334         // Give write permissions
335         tmpfile, err = MakeTempCSVFile([][]string{{groupName, userEmail, "can_write"}})
336         c.Assert(err, IsNil)
337         defer os.Remove(tmpfile.Name()) // clean up
338         s.cfg.Path = tmpfile.Name()
339         err = doMain(s.cfg)
340         c.Assert(err, IsNil)
341         // Check permissions
342         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_read"), Equals, false)
343         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_write"), Equals, true)
344         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_manage"), Equals, false)
345
346         // Give manage permissions
347         tmpfile, err = MakeTempCSVFile([][]string{{groupName, userEmail, "can_manage"}})
348         c.Assert(err, IsNil)
349         defer os.Remove(tmpfile.Name()) // clean up
350         s.cfg.Path = tmpfile.Name()
351         err = doMain(s.cfg)
352         c.Assert(err, IsNil)
353         // Check permissions
354         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_read"), Equals, false)
355         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_write"), Equals, false)
356         c.Assert(GroupMembershipExists(s.cfg.Client, userUUID, groupUUID, "can_manage"), Equals, true)
357 }
358
359 // The absence of a user membership on the CSV file implies its removal
360 func (s *TestSuite) TestMembershipRemoval(c *C) {
361         localUserEmail := s.users[arvadostest.ActiveUserUUID].Email
362         localUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
363         remoteUserEmail := s.users[arvadostest.FederatedActiveUserUUID].Email
364         remoteUserUUID := s.users[arvadostest.FederatedActiveUserUUID].UUID
365         data := [][]string{
366                 {"TestGroup1", localUserEmail},
367                 {"TestGroup1", remoteUserEmail},
368                 {"TestGroup2", localUserEmail},
369                 {"TestGroup2", remoteUserEmail},
370         }
371         tmpfile, err := MakeTempCSVFile(data)
372         c.Assert(err, IsNil)
373         defer os.Remove(tmpfile.Name()) // clean up
374         s.cfg.Path = tmpfile.Name()
375         err = doMain(s.cfg)
376         c.Assert(err, IsNil)
377         // Confirm that memberships exist
378         for _, groupName := range []string{"TestGroup1", "TestGroup2"} {
379                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
380                 c.Assert(err, IsNil)
381                 c.Assert(groupUUID, Not(Equals), "")
382                 c.Assert(GroupMembershipExists(s.cfg.Client, localUserUUID, groupUUID, "can_write"), Equals, true)
383                 c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID, "can_write"), Equals, true)
384         }
385         // New CSV with some previous membership missing
386         data = [][]string{
387                 {"TestGroup1", localUserEmail},
388                 {"TestGroup2", remoteUserEmail},
389         }
390         tmpfile2, err := MakeTempCSVFile(data)
391         c.Assert(err, IsNil)
392         defer os.Remove(tmpfile2.Name()) // clean up
393         s.cfg.Path = tmpfile2.Name()
394         err = doMain(s.cfg)
395         c.Assert(err, IsNil)
396         // Confirm TestGroup1 memberships
397         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
398         c.Assert(err, IsNil)
399         c.Assert(groupUUID, Not(Equals), "")
400         c.Assert(GroupMembershipExists(s.cfg.Client, localUserUUID, groupUUID, "can_write"), Equals, true)
401         c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID, "can_write"), Equals, false)
402         // Confirm TestGroup1 memberships
403         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup2")
404         c.Assert(err, IsNil)
405         c.Assert(groupUUID, Not(Equals), "")
406         c.Assert(GroupMembershipExists(s.cfg.Client, localUserUUID, groupUUID, "can_write"), Equals, false)
407         c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID, "can_write"), Equals, true)
408 }
409
410 // If a group doesn't exist on the system, create it before adding users
411 func (s *TestSuite) TestAutoCreateGroupWhenNotExisting(c *C) {
412         groupName := "Testers"
413         // Confirm that group doesn't exist
414         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
415         c.Assert(err, IsNil)
416         c.Assert(groupUUID, Equals, "")
417         // Make a tmp CSV file
418         data := [][]string{
419                 {groupName, s.users[arvadostest.ActiveUserUUID].Email},
420         }
421         tmpfile, err := MakeTempCSVFile(data)
422         c.Assert(err, IsNil)
423         defer os.Remove(tmpfile.Name()) // clean up
424         s.cfg.Path = tmpfile.Name()
425         err = doMain(s.cfg)
426         c.Assert(err, IsNil)
427         // "Testers" group should now exist
428         groupUUID, err = RemoteGroupExists(s.cfg, groupName)
429         c.Assert(err, IsNil)
430         c.Assert(groupUUID, Not(Equals), "")
431         // active user should be a member
432         c.Assert(GroupMembershipExists(s.cfg.Client, arvadostest.ActiveUserUUID, groupUUID, "can_write"), Equals, true)
433 }
434
435 // Users listed on the file that don't exist on the system are ignored
436 func (s *TestSuite) TestIgnoreNonexistantUsers(c *C) {
437         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
438         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
439         // Confirm that group doesn't exist
440         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup4")
441         c.Assert(err, IsNil)
442         c.Assert(groupUUID, Equals, "")
443         // Create file & run command
444         data := [][]string{
445                 {"TestGroup4", "nonexistantuser@unknowndomain.com"}, // Processed first
446                 {"TestGroup4", activeUserEmail},
447         }
448         tmpfile, err := MakeTempCSVFile(data)
449         c.Assert(err, IsNil)
450         defer os.Remove(tmpfile.Name()) // clean up
451         s.cfg.Path = tmpfile.Name()
452         err = doMain(s.cfg)
453         c.Assert(err, IsNil)
454         // Confirm that memberships exist
455         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
456         c.Assert(err, IsNil)
457         c.Assert(groupUUID, Not(Equals), "")
458         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID, "can_write"), Equals, true)
459 }
460
461 // Users listed on the file that don't exist on the system are ignored
462 func (s *TestSuite) TestIgnoreEmptyFields(c *C) {
463         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
464         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
465         // Confirm that group doesn't exist
466         for _, groupName := range []string{"TestGroup4", "TestGroup5"} {
467                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
468                 c.Assert(err, IsNil)
469                 c.Assert(groupUUID, Equals, "")
470         }
471         // Create file & run command
472         data := [][]string{
473                 {"", activeUserEmail},               // Empty field
474                 {"TestGroup5", ""},                  // Empty field
475                 {"TestGroup5", activeUserEmail, ""}, // Empty 3rd field: is optional but cannot be empty
476                 {"TestGroup4", activeUserEmail},
477         }
478         tmpfile, err := MakeTempCSVFile(data)
479         c.Assert(err, IsNil)
480         defer os.Remove(tmpfile.Name()) // clean up
481         s.cfg.Path = tmpfile.Name()
482         err = doMain(s.cfg)
483         c.Assert(err, IsNil)
484         // Confirm that records about TestGroup5 were skipped
485         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup5")
486         c.Assert(err, IsNil)
487         c.Assert(groupUUID, Equals, "")
488         // Confirm that membership exists
489         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
490         c.Assert(err, IsNil)
491         c.Assert(groupUUID, Not(Equals), "")
492         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID, "can_write"), Equals, true)
493 }
494
495 // Instead of emails, use username as identifier
496 func (s *TestSuite) TestUseUsernames(c *C) {
497         activeUserName := s.users[arvadostest.ActiveUserUUID].Username
498         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
499         // Confirm that group doesn't exist
500         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
501         c.Assert(err, IsNil)
502         c.Assert(groupUUID, Equals, "")
503         // Create file & run command
504         data := [][]string{
505                 {"TestGroup1", activeUserName},
506         }
507         tmpfile, err := MakeTempCSVFile(data)
508         c.Assert(err, IsNil)
509         defer os.Remove(tmpfile.Name()) // clean up
510         s.cfg.Path = tmpfile.Name()
511         s.cfg.UserID = "username"
512         err = doMain(s.cfg)
513         s.cfg.UserID = "email"
514         c.Assert(err, IsNil)
515         // Confirm that memberships exist
516         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup1")
517         c.Assert(err, IsNil)
518         c.Assert(groupUUID, Not(Equals), "")
519         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID, "can_write"), Equals, true)
520 }