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