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