16435: Allows 2 or 3 fields per record on the CSV file.
[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) 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:  "can_write",
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.ParentGroupUUID,
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), 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 // The absence of a user membership on the CSV file implies its removal
283 func (s *TestSuite) TestMembershipRemoval(c *C) {
284         localUserEmail := s.users[arvadostest.ActiveUserUUID].Email
285         localUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
286         remoteUserEmail := s.users[arvadostest.FederatedActiveUserUUID].Email
287         remoteUserUUID := s.users[arvadostest.FederatedActiveUserUUID].UUID
288         data := [][]string{
289                 {"TestGroup1", localUserEmail},
290                 {"TestGroup1", remoteUserEmail},
291                 {"TestGroup2", localUserEmail},
292                 {"TestGroup2", remoteUserEmail},
293         }
294         tmpfile, err := MakeTempCSVFile(data)
295         c.Assert(err, IsNil)
296         defer os.Remove(tmpfile.Name()) // clean up
297         s.cfg.Path = tmpfile.Name()
298         err = doMain(s.cfg)
299         c.Assert(err, IsNil)
300         // Confirm that memberships exist
301         for _, groupName := range []string{"TestGroup1", "TestGroup2"} {
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, localUserUUID, groupUUID), Equals, true)
306                 c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID), Equals, true)
307         }
308         // New CSV with some previous membership missing
309         data = [][]string{
310                 {"TestGroup1", localUserEmail},
311                 {"TestGroup2", remoteUserEmail},
312         }
313         tmpfile2, err := MakeTempCSVFile(data)
314         c.Assert(err, IsNil)
315         defer os.Remove(tmpfile2.Name()) // clean up
316         s.cfg.Path = tmpfile2.Name()
317         err = doMain(s.cfg)
318         c.Assert(err, IsNil)
319         // Confirm TestGroup1 memberships
320         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
321         c.Assert(err, IsNil)
322         c.Assert(groupUUID, Not(Equals), "")
323         c.Assert(GroupMembershipExists(s.cfg.Client, localUserUUID, groupUUID), Equals, true)
324         c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID), Equals, false)
325         // Confirm TestGroup1 memberships
326         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup2")
327         c.Assert(err, IsNil)
328         c.Assert(groupUUID, Not(Equals), "")
329         c.Assert(GroupMembershipExists(s.cfg.Client, localUserUUID, groupUUID), Equals, false)
330         c.Assert(GroupMembershipExists(s.cfg.Client, remoteUserUUID, groupUUID), Equals, true)
331 }
332
333 // If a group doesn't exist on the system, create it before adding users
334 func (s *TestSuite) TestAutoCreateGroupWhenNotExisting(c *C) {
335         groupName := "Testers"
336         // Confirm that group doesn't exist
337         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
338         c.Assert(err, IsNil)
339         c.Assert(groupUUID, Equals, "")
340         // Make a tmp CSV file
341         data := [][]string{
342                 {groupName, s.users[arvadostest.ActiveUserUUID].Email},
343         }
344         tmpfile, err := MakeTempCSVFile(data)
345         c.Assert(err, IsNil)
346         defer os.Remove(tmpfile.Name()) // clean up
347         s.cfg.Path = tmpfile.Name()
348         err = doMain(s.cfg)
349         c.Assert(err, IsNil)
350         // "Testers" group should now exist
351         groupUUID, err = RemoteGroupExists(s.cfg, groupName)
352         c.Assert(err, IsNil)
353         c.Assert(groupUUID, Not(Equals), "")
354         // active user should be a member
355         c.Assert(GroupMembershipExists(s.cfg.Client, arvadostest.ActiveUserUUID, groupUUID), Equals, true)
356 }
357
358 // Users listed on the file that don't exist on the system are ignored
359 func (s *TestSuite) TestIgnoreNonexistantUsers(c *C) {
360         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
361         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
362         // Confirm that group doesn't exist
363         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup4")
364         c.Assert(err, IsNil)
365         c.Assert(groupUUID, Equals, "")
366         // Create file & run command
367         data := [][]string{
368                 {"TestGroup4", "nonexistantuser@unknowndomain.com"}, // Processed first
369                 {"TestGroup4", activeUserEmail},
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         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
379         c.Assert(err, IsNil)
380         c.Assert(groupUUID, Not(Equals), "")
381         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
382 }
383
384 // Users listed on the file that don't exist on the system are ignored
385 func (s *TestSuite) TestIgnoreEmptyFields(c *C) {
386         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
387         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
388         // Confirm that group doesn't exist
389         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup4")
390         c.Assert(err, IsNil)
391         c.Assert(groupUUID, Equals, "")
392         // Create file & run command
393         data := [][]string{
394                 {"", activeUserEmail}, // Empty field
395                 {"TestGroup5", ""},    // Empty field
396                 {"TestGroup4", activeUserEmail},
397         }
398         tmpfile, err := MakeTempCSVFile(data)
399         c.Assert(err, IsNil)
400         defer os.Remove(tmpfile.Name()) // clean up
401         s.cfg.Path = tmpfile.Name()
402         err = doMain(s.cfg)
403         c.Assert(err, IsNil)
404         // Confirm that memberships exist
405         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
406         c.Assert(err, IsNil)
407         c.Assert(groupUUID, Not(Equals), "")
408         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
409 }
410
411 // Instead of emails, use username as identifier
412 func (s *TestSuite) TestUseUsernames(c *C) {
413         activeUserName := s.users[arvadostest.ActiveUserUUID].Username
414         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
415         // Confirm that group doesn't exist
416         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
417         c.Assert(err, IsNil)
418         c.Assert(groupUUID, Equals, "")
419         // Create file & run command
420         data := [][]string{
421                 {"TestGroup1", activeUserName},
422         }
423         tmpfile, err := MakeTempCSVFile(data)
424         c.Assert(err, IsNil)
425         defer os.Remove(tmpfile.Name()) // clean up
426         s.cfg.Path = tmpfile.Name()
427         s.cfg.UserID = "username"
428         err = doMain(s.cfg)
429         s.cfg.UserID = "email"
430         c.Assert(err, IsNil)
431         // Confirm that memberships exist
432         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup1")
433         c.Assert(err, IsNil)
434         c.Assert(groupUUID, Not(Equals), "")
435         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
436 }