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