Rename the group sync tool to follow our standard naming scheme.
[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 // Clean any membership link and remote group created by the test
87 func (s *TestSuite) TearDownTest(c *C) {
88         var dst interface{}
89         // Reset database to fixture state after every test run.
90         err := s.cfg.Client.RequestAndDecode(&dst, "POST", "/database/reset", nil, nil)
91         c.Assert(err, IsNil)
92 }
93
94 var _ = Suite(&TestSuite{})
95
96 // MakeTempCVSFile creates a temp file with data as comma separated values
97 func MakeTempCSVFile(data [][]string) (f *os.File, err error) {
98         f, err = ioutil.TempFile("", "test_sync_remote_groups")
99         if err != nil {
100                 return
101         }
102         for _, line := range data {
103                 fmt.Fprintf(f, "%s\n", strings.Join(line, ","))
104         }
105         err = f.Close()
106         return
107 }
108
109 // GroupMembershipExists checks that both needed links exist between user and group
110 func GroupMembershipExists(ac *arvados.Client, userUUID string, groupUUID string) bool {
111         ll := LinkList{}
112         // Check Group -> User can_read permission
113         params := arvados.ResourceListParams{
114                 Filters: []arvados.Filter{{
115                         Attr:     "link_class",
116                         Operator: "=",
117                         Operand:  "permission",
118                 }, {
119                         Attr:     "tail_uuid",
120                         Operator: "=",
121                         Operand:  groupUUID,
122                 }, {
123                         Attr:     "name",
124                         Operator: "=",
125                         Operand:  "can_read",
126                 }, {
127                         Attr:     "head_uuid",
128                         Operator: "=",
129                         Operand:  userUUID,
130                 }},
131         }
132         ac.RequestAndDecode(&ll, "GET", "/arvados/v1/links", nil, params)
133         if ll.Len() != 1 {
134                 return false
135         }
136         // Check User -> Group can_write permission
137         params = arvados.ResourceListParams{
138                 Filters: []arvados.Filter{{
139                         Attr:     "link_class",
140                         Operator: "=",
141                         Operand:  "permission",
142                 }, {
143                         Attr:     "head_uuid",
144                         Operator: "=",
145                         Operand:  groupUUID,
146                 }, {
147                         Attr:     "name",
148                         Operator: "=",
149                         Operand:  "can_write",
150                 }, {
151                         Attr:     "tail_uuid",
152                         Operator: "=",
153                         Operand:  userUUID,
154                 }},
155         }
156         ac.RequestAndDecode(&ll, "GET", "/arvados/v1/links", nil, params)
157         if ll.Len() != 1 {
158                 return false
159         }
160         return true
161 }
162
163 // If named group exists, return its UUID
164 func RemoteGroupExists(cfg *ConfigParams, groupName string) (uuid string, err error) {
165         gl := arvados.GroupList{}
166         params := arvados.ResourceListParams{
167                 Filters: []arvados.Filter{{
168                         Attr:     "name",
169                         Operator: "=",
170                         Operand:  groupName,
171                 }, {
172                         Attr:     "owner_uuid",
173                         Operator: "=",
174                         Operand:  cfg.ParentGroupUUID,
175                 }, {
176                         Attr:     "group_class",
177                         Operator: "=",
178                         Operand:  "role",
179                 }},
180         }
181         err = cfg.Client.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params)
182         if err != nil {
183                 return "", err
184         }
185         if gl.ItemsAvailable == 0 {
186                 // No group with this name
187                 uuid = ""
188         } else if gl.ItemsAvailable == 1 {
189                 // Group found
190                 uuid = gl.Items[0].UUID
191         } else {
192                 // This should never happen
193                 uuid = ""
194                 err = fmt.Errorf("more than 1 group found with the same name and parent")
195         }
196         return
197 }
198
199 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
200         cfg := ConfigParams{}
201         os.Args = []string{"cmd", "-verbose", "/tmp/somefile.csv"}
202         err := ParseFlags(&cfg)
203         c.Assert(err, IsNil)
204         c.Check(cfg.Path, Equals, "/tmp/somefile.csv")
205         c.Check(cfg.Verbose, Equals, true)
206 }
207
208 func (s *TestSuite) TestParseFlagsWithoutPositionalArgument(c *C) {
209         os.Args = []string{"cmd", "-verbose"}
210         err := ParseFlags(&ConfigParams{})
211         c.Assert(err, NotNil)
212 }
213
214 func (s *TestSuite) TestGetUserID(c *C) {
215         u := arvados.User{
216                 Email:    "testuser@example.com",
217                 Username: "Testuser",
218         }
219         email, err := GetUserID(u, "email")
220         c.Assert(err, IsNil)
221         c.Check(email, Equals, "testuser@example.com")
222         _, err = GetUserID(u, "bogus")
223         c.Assert(err, NotNil)
224 }
225
226 func (s *TestSuite) TestGetConfig(c *C) {
227         os.Args = []string{"cmd", "/tmp/somefile.csv"}
228         cfg, err := GetConfig()
229         c.Assert(err, IsNil)
230         c.Check(cfg.SysUserUUID, NotNil)
231         c.Check(cfg.Client, NotNil)
232         c.Check(cfg.ParentGroupUUID, NotNil)
233         c.Check(cfg.ParentGroupName, Equals, "Externally synchronized groups")
234 }
235
236 // Ignore leading & trailing spaces on group & users names
237 func (s *TestSuite) TestIgnoreSpaces(c *C) {
238         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
239         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
240         // Confirm that the groups don't exist
241         for _, groupName := range []string{"TestGroup1", "TestGroup2", "Test Group 3"} {
242                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
243                 c.Assert(err, IsNil)
244                 c.Assert(groupUUID, Equals, "")
245         }
246         data := [][]string{
247                 {" TestGroup1", activeUserEmail},
248                 {"TestGroup2 ", " " + activeUserEmail},
249                 {" Test Group 3 ", activeUserEmail + " "},
250         }
251         tmpfile, err := MakeTempCSVFile(data)
252         c.Assert(err, IsNil)
253         defer os.Remove(tmpfile.Name()) // clean up
254         s.cfg.Path = tmpfile.Name()
255         err = doMain(s.cfg)
256         c.Assert(err, IsNil)
257         // Check that 3 groups were created correctly, and have the active user as
258         // a member.
259         for _, groupName := range []string{"TestGroup1", "TestGroup2", "Test Group 3"} {
260                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
261                 c.Assert(err, IsNil)
262                 c.Assert(groupUUID, Not(Equals), "")
263                 c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
264         }
265 }
266
267 // The absence of a user membership on the CSV file implies its removal
268 func (s *TestSuite) TestMembershipRemoval(c *C) {
269         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
270         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
271         data := [][]string{
272                 {"TestGroup1", activeUserEmail},
273                 {"TestGroup2", activeUserEmail},
274         }
275         tmpfile, err := MakeTempCSVFile(data)
276         c.Assert(err, IsNil)
277         defer os.Remove(tmpfile.Name()) // clean up
278         s.cfg.Path = tmpfile.Name()
279         err = doMain(s.cfg)
280         c.Assert(err, IsNil)
281         // Confirm that memberships exist
282         for _, groupName := range []string{"TestGroup1", "TestGroup2"} {
283                 groupUUID, err := RemoteGroupExists(s.cfg, groupName)
284                 c.Assert(err, IsNil)
285                 c.Assert(groupUUID, Not(Equals), "")
286                 c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
287         }
288         // New CSV with one previous membership missing
289         data = [][]string{
290                 {"TestGroup1", activeUserEmail},
291         }
292         tmpfile2, err := MakeTempCSVFile(data)
293         c.Assert(err, IsNil)
294         defer os.Remove(tmpfile2.Name()) // clean up
295         s.cfg.Path = tmpfile2.Name()
296         err = doMain(s.cfg)
297         c.Assert(err, IsNil)
298         // Confirm TestGroup1 membership still exist
299         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
300         c.Assert(err, IsNil)
301         c.Assert(groupUUID, Not(Equals), "")
302         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
303         // Confirm TestGroup2 membership was removed
304         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup2")
305         c.Assert(err, IsNil)
306         c.Assert(groupUUID, Not(Equals), "")
307         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, false)
308 }
309
310 // If a group doesn't exist on the system, create it before adding users
311 func (s *TestSuite) TestAutoCreateGroupWhenNotExisting(c *C) {
312         groupName := "Testers"
313         // Confirm that group doesn't exist
314         groupUUID, err := RemoteGroupExists(s.cfg, groupName)
315         c.Assert(err, IsNil)
316         c.Assert(groupUUID, Equals, "")
317         // Make a tmp CSV file
318         data := [][]string{
319                 {groupName, s.users[arvadostest.ActiveUserUUID].Email},
320         }
321         tmpfile, err := MakeTempCSVFile(data)
322         c.Assert(err, IsNil)
323         defer os.Remove(tmpfile.Name()) // clean up
324         s.cfg.Path = tmpfile.Name()
325         err = doMain(s.cfg)
326         c.Assert(err, IsNil)
327         // "Testers" group should now exist
328         groupUUID, err = RemoteGroupExists(s.cfg, groupName)
329         c.Assert(err, IsNil)
330         c.Assert(groupUUID, Not(Equals), "")
331         // active user should be a member
332         c.Assert(GroupMembershipExists(s.cfg.Client, arvadostest.ActiveUserUUID, groupUUID), Equals, true)
333 }
334
335 // Users listed on the file that don't exist on the system are ignored
336 func (s *TestSuite) TestIgnoreNonexistantUsers(c *C) {
337         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
338         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
339         // Confirm that group doesn't exist
340         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup4")
341         c.Assert(err, IsNil)
342         c.Assert(groupUUID, Equals, "")
343         // Create file & run command
344         data := [][]string{
345                 {"TestGroup4", "nonexistantuser@unknowndomain.com"}, // Processed first
346                 {"TestGroup4", activeUserEmail},
347         }
348         tmpfile, err := MakeTempCSVFile(data)
349         c.Assert(err, IsNil)
350         defer os.Remove(tmpfile.Name()) // clean up
351         s.cfg.Path = tmpfile.Name()
352         err = doMain(s.cfg)
353         c.Assert(err, IsNil)
354         // Confirm that memberships exist
355         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
356         c.Assert(err, IsNil)
357         c.Assert(groupUUID, Not(Equals), "")
358         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
359 }
360
361 // Users listed on the file that don't exist on the system are ignored
362 func (s *TestSuite) TestIgnoreEmptyFields(c *C) {
363         activeUserEmail := s.users[arvadostest.ActiveUserUUID].Email
364         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
365         // Confirm that group doesn't exist
366         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup4")
367         c.Assert(err, IsNil)
368         c.Assert(groupUUID, Equals, "")
369         // Create file & run command
370         data := [][]string{
371                 {"", activeUserEmail}, // Empty field
372                 {"TestGroup5", ""},    // Empty field
373                 {"TestGroup4", activeUserEmail},
374         }
375         tmpfile, err := MakeTempCSVFile(data)
376         c.Assert(err, IsNil)
377         defer os.Remove(tmpfile.Name()) // clean up
378         s.cfg.Path = tmpfile.Name()
379         err = doMain(s.cfg)
380         c.Assert(err, IsNil)
381         // Confirm that memberships exist
382         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup4")
383         c.Assert(err, IsNil)
384         c.Assert(groupUUID, Not(Equals), "")
385         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
386 }
387
388 // Instead of emails, use username as identifier
389 func (s *TestSuite) TestUseUsernames(c *C) {
390         activeUserName := s.users[arvadostest.ActiveUserUUID].Username
391         activeUserUUID := s.users[arvadostest.ActiveUserUUID].UUID
392         // Confirm that group doesn't exist
393         groupUUID, err := RemoteGroupExists(s.cfg, "TestGroup1")
394         c.Assert(err, IsNil)
395         c.Assert(groupUUID, Equals, "")
396         // Create file & run command
397         data := [][]string{
398                 {"TestGroup1", activeUserName},
399         }
400         tmpfile, err := MakeTempCSVFile(data)
401         c.Assert(err, IsNil)
402         defer os.Remove(tmpfile.Name()) // clean up
403         s.cfg.Path = tmpfile.Name()
404         s.cfg.UserID = "username"
405         err = doMain(s.cfg)
406         s.cfg.UserID = "email"
407         c.Assert(err, IsNil)
408         // Confirm that memberships exist
409         groupUUID, err = RemoteGroupExists(s.cfg, "TestGroup1")
410         c.Assert(err, IsNil)
411         c.Assert(groupUUID, Not(Equals), "")
412         c.Assert(GroupMembershipExists(s.cfg.Client, activeUserUUID, groupUUID), Equals, true)
413 }