18858: Fixes positional argument retrieval on sync-users & sync-groups.
[arvados.git] / tools / sync-groups / sync-groups.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         "bytes"
9         "encoding/csv"
10         "encoding/json"
11         "flag"
12         "fmt"
13         "io"
14         "log"
15         "net/url"
16         "os"
17         "strings"
18
19         "git.arvados.org/arvados.git/lib/cmd"
20         "git.arvados.org/arvados.git/sdk/go/arvados"
21 )
22
23 var version = "dev"
24
25 type resourceList interface {
26         Len() int
27         GetItems() []interface{}
28 }
29
30 // GroupPermissions maps permission levels on groups (can_read, can_write, can_manage)
31 type GroupPermissions map[string]bool
32
33 // GroupInfo tracks previous and current member's permissions on a particular Group
34 type GroupInfo struct {
35         Group           arvados.Group
36         PreviousMembers map[string]GroupPermissions
37         CurrentMembers  map[string]GroupPermissions
38 }
39
40 // GetUserID returns the correct user id value depending on the selector
41 func GetUserID(u arvados.User, idSelector string) (string, error) {
42         switch idSelector {
43         case "email":
44                 return u.Email, nil
45         case "username":
46                 return u.Username, nil
47         default:
48                 return "", fmt.Errorf("cannot identify user by %q selector", idSelector)
49         }
50 }
51
52 // UserList implements resourceList interface
53 type UserList struct {
54         arvados.UserList
55 }
56
57 // Len returns the amount of items this list holds
58 func (l UserList) Len() int {
59         return len(l.Items)
60 }
61
62 // GetItems returns the list of items
63 func (l UserList) GetItems() (out []interface{}) {
64         for _, item := range l.Items {
65                 out = append(out, item)
66         }
67         return
68 }
69
70 // GroupList implements resourceList interface
71 type GroupList struct {
72         arvados.GroupList
73 }
74
75 // Len returns the amount of items this list holds
76 func (l GroupList) Len() int {
77         return len(l.Items)
78 }
79
80 // GetItems returns the list of items
81 func (l GroupList) GetItems() (out []interface{}) {
82         for _, item := range l.Items {
83                 out = append(out, item)
84         }
85         return
86 }
87
88 // LinkList implements resourceList interface
89 type LinkList struct {
90         arvados.LinkList
91 }
92
93 // Len returns the amount of items this list holds
94 func (l LinkList) Len() int {
95         return len(l.Items)
96 }
97
98 // GetItems returns the list of items
99 func (l LinkList) GetItems() (out []interface{}) {
100         for _, item := range l.Items {
101                 out = append(out, item)
102         }
103         return
104 }
105
106 func main() {
107         // Parse & validate arguments, set up arvados client.
108         cfg, err := GetConfig()
109         if err != nil {
110                 log.Fatalf("%v", err)
111         }
112
113         if err := doMain(&cfg); err != nil {
114                 log.Fatalf("%v", err)
115         }
116 }
117
118 // ConfigParams holds configuration data for this tool
119 type ConfigParams struct {
120         Path            string
121         UserID          string
122         Verbose         bool
123         CaseInsensitive bool
124         ParentGroupUUID string
125         ParentGroupName string
126         SysUserUUID     string
127         Client          *arvados.Client
128 }
129
130 // ParseFlags parses and validates command line arguments
131 func ParseFlags(config *ConfigParams) error {
132         // Acceptable attributes to identify a user on the CSV file
133         userIDOpts := map[string]bool{
134                 "email":    true, // default
135                 "username": true,
136         }
137
138         flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
139
140         // Set up usage message
141         flags.Usage = func() {
142                 usageStr := `Synchronize remote groups into Arvados from a CSV format file with 3 columns:
143   * 1st: Group name
144   * 2nd: User identifier
145   * 3rd (Optional): User permission on the group: can_read, can_write or can_manage. (Default: can_write)`
146                 fmt.Fprintf(flags.Output(), "%s\n\n", usageStr)
147                 fmt.Fprintf(flags.Output(), "Usage:\n%s [OPTIONS] <input-file.csv>\n\n", os.Args[0])
148                 fmt.Fprintf(flags.Output(), "Options:\n")
149                 flags.PrintDefaults()
150         }
151
152         // Set up option flags
153         userID := flags.String(
154                 "user-id",
155                 "email",
156                 "Attribute by which every user is identified. Valid values are: email and username.")
157         caseInsensitive := flags.Bool(
158                 "case-insensitive",
159                 false,
160                 "Performs case insensitive matching on user IDs. Off by default.")
161         verbose := flags.Bool(
162                 "verbose",
163                 false,
164                 "Log informational messages. Off by default.")
165         getVersion := flags.Bool(
166                 "version",
167                 false,
168                 "Print version information and exit.")
169         parentGroupUUID := flags.String(
170                 "parent-group-uuid",
171                 "",
172                 "Use given group UUID as a parent for the remote groups. Should be owned by the system user. If not specified, a group named '"+config.ParentGroupName+"' will be used (and created if nonexistant).")
173
174         if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "input-file.csv", os.Stderr); !ok {
175                 os.Exit(code)
176         } else if *getVersion {
177                 fmt.Printf("%s %s\n", os.Args[0], version)
178                 os.Exit(0)
179         }
180
181         // Input file as a required positional argument
182         if flags.NArg() == 0 {
183                 return fmt.Errorf("please provide a path to an input file")
184         } else if flags.NArg() > 1 {
185                 return fmt.Errorf("please provide just one input file argument")
186         }
187         srcPath := &os.Args[len(os.Args)-1]
188
189         // Validations
190         if *srcPath == "" {
191                 return fmt.Errorf("input file path invalid")
192         }
193         if !userIDOpts[*userID] {
194                 var options []string
195                 for opt := range userIDOpts {
196                         options = append(options, opt)
197                 }
198                 return fmt.Errorf("user ID must be one of: %s", strings.Join(options, ", "))
199         }
200
201         config.Path = *srcPath
202         config.ParentGroupUUID = *parentGroupUUID
203         config.UserID = *userID
204         config.Verbose = *verbose
205         config.CaseInsensitive = *caseInsensitive
206
207         return nil
208 }
209
210 // SetParentGroup finds/create parent group of all remote groups
211 func SetParentGroup(cfg *ConfigParams) error {
212         var parentGroup arvados.Group
213         if cfg.ParentGroupUUID == "" {
214                 // UUID not provided, search for preexisting parent group
215                 var gl GroupList
216                 params := arvados.ResourceListParams{
217                         Filters: []arvados.Filter{{
218                                 Attr:     "name",
219                                 Operator: "=",
220                                 Operand:  cfg.ParentGroupName,
221                         }, {
222                                 Attr:     "owner_uuid",
223                                 Operator: "=",
224                                 Operand:  cfg.SysUserUUID,
225                         }},
226                 }
227                 if err := cfg.Client.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params); err != nil {
228                         return fmt.Errorf("error searching for parent group: %s", err)
229                 }
230                 if len(gl.Items) == 0 {
231                         // Default parent group does not exist, create it.
232                         if cfg.Verbose {
233                                 log.Println("Default parent group not found, creating...")
234                         }
235                         groupData := map[string]string{
236                                 "name":        cfg.ParentGroupName,
237                                 "owner_uuid":  cfg.SysUserUUID,
238                                 "group_class": "role",
239                         }
240                         if err := CreateGroup(cfg, &parentGroup, groupData); err != nil {
241                                 return fmt.Errorf("error creating system user owned group named %q: %s", groupData["name"], err)
242                         }
243                 } else if len(gl.Items) == 1 {
244                         // Default parent group found.
245                         parentGroup = gl.Items[0]
246                 } else {
247                         // This should never happen, as there's an unique index for
248                         // (owner_uuid, name) on groups.
249                         return fmt.Errorf("bug: found %d groups owned by system user and named %q", len(gl.Items), cfg.ParentGroupName)
250                 }
251                 cfg.ParentGroupUUID = parentGroup.UUID
252         } else {
253                 // UUID provided. Check if exists and if it's owned by system user
254                 if err := GetGroup(cfg, &parentGroup, cfg.ParentGroupUUID); err != nil {
255                         return fmt.Errorf("error searching for parent group with UUID %q: %s", cfg.ParentGroupUUID, err)
256                 }
257                 if parentGroup.OwnerUUID != cfg.SysUserUUID {
258                         return fmt.Errorf("parent group %q (%s) must be owned by system user", parentGroup.Name, cfg.ParentGroupUUID)
259                 }
260         }
261         return nil
262 }
263
264 // GetConfig sets up a ConfigParams struct
265 func GetConfig() (config ConfigParams, err error) {
266         config.ParentGroupName = "Externally synchronized groups"
267
268         // Command arguments
269         err = ParseFlags(&config)
270         if err != nil {
271                 return config, err
272         }
273
274         // Arvados Client setup
275         config.Client = arvados.NewClientFromEnv()
276
277         // Check current user permissions & get System user's UUID
278         u, err := config.Client.CurrentUser()
279         if err != nil {
280                 return config, fmt.Errorf("error getting the current user: %s", err)
281         }
282         if !u.IsActive || !u.IsAdmin {
283                 return config, fmt.Errorf("current user (%s) is not an active admin user", u.UUID)
284         }
285
286         var ac struct{ ClusterID string }
287         err = config.Client.RequestAndDecode(&ac, "GET", "arvados/v1/config", nil, nil)
288         if err != nil {
289                 return config, fmt.Errorf("error getting the exported config: %s", err)
290         }
291         config.SysUserUUID = ac.ClusterID + "-tpzed-000000000000000"
292
293         // Set up remote groups' parent
294         if err = SetParentGroup(&config); err != nil {
295                 return config, err
296         }
297
298         return config, nil
299 }
300
301 func doMain(cfg *ConfigParams) error {
302         // Try opening the input file early, just in case there's a problem.
303         f, err := os.Open(cfg.Path)
304         if err != nil {
305                 return fmt.Errorf("%s", err)
306         }
307         defer f.Close()
308
309         iCaseLog := ""
310         if cfg.UserID == "username" && cfg.CaseInsensitive {
311                 iCaseLog = " - username matching requested to be case-insensitive"
312         }
313         log.Printf("%s %s started. Using %q as users id and parent group UUID %q%s", os.Args[0], version, cfg.UserID, cfg.ParentGroupUUID, iCaseLog)
314
315         // Get the complete user list to minimize API Server requests
316         allUsers := make(map[string]arvados.User)
317         userIDToUUID := make(map[string]string) // Index by email or username
318         results, err := GetAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
319         if err != nil {
320                 return fmt.Errorf("error getting user list: %s", err)
321         }
322         log.Printf("Found %d users", len(results))
323         for _, item := range results {
324                 u := item.(arvados.User)
325                 allUsers[u.UUID] = u
326                 uID, err := GetUserID(u, cfg.UserID)
327                 if err != nil {
328                         return err
329                 }
330                 if cfg.UserID == "username" && uID != "" && cfg.CaseInsensitive {
331                         uID = strings.ToLower(uID)
332                         if uuid, found := userIDToUUID[uID]; found {
333                                 return fmt.Errorf("case insensitive collision for username %q between %q and %q", uID, u.UUID, uuid)
334                         }
335                 }
336                 userIDToUUID[uID] = u.UUID
337                 if cfg.Verbose {
338                         log.Printf("Seen user %q (%s)", u.Username, u.UUID)
339                 }
340         }
341
342         // Get remote groups and their members
343         remoteGroups, groupNameToUUID, err := GetRemoteGroups(cfg, allUsers)
344         if err != nil {
345                 return err
346         }
347         log.Printf("Found %d remote groups", len(remoteGroups))
348         if cfg.Verbose {
349                 for groupUUID := range remoteGroups {
350                         log.Printf("- Group %q: %d users", remoteGroups[groupUUID].Group.Name, len(remoteGroups[groupUUID].PreviousMembers))
351                 }
352         }
353
354         membershipsRemoved := 0
355
356         // Read the CSV file
357         groupsCreated, membershipsAdded, membershipsSkipped, err := ProcessFile(cfg, f, userIDToUUID, groupNameToUUID, remoteGroups, allUsers)
358         if err != nil {
359                 return err
360         }
361
362         // Remove previous members not listed on this run
363         for groupUUID := range remoteGroups {
364                 gi := remoteGroups[groupUUID]
365                 evictedMemberPerms := subtract(gi.PreviousMembers, gi.CurrentMembers)
366                 groupName := gi.Group.Name
367                 if len(evictedMemberPerms) > 0 {
368                         log.Printf("Removing permissions from %d users on group %q", len(evictedMemberPerms), groupName)
369                 }
370                 for member := range evictedMemberPerms {
371                         var perms []string
372                         completeMembershipRemoval := false
373                         if _, ok := gi.CurrentMembers[member]; !ok {
374                                 completeMembershipRemoval = true
375                                 membershipsRemoved++
376                         } else {
377                                 // Collect which user->group permission links should be removed
378                                 for p := range evictedMemberPerms[member] {
379                                         if evictedMemberPerms[member][p] {
380                                                 perms = append(perms, p)
381                                         }
382                                 }
383                                 membershipsRemoved += len(perms)
384                         }
385                         if err := RemoveMemberLinksFromGroup(cfg, allUsers[userIDToUUID[member]],
386                                 perms, completeMembershipRemoval, gi.Group); err != nil {
387                                 return err
388                         }
389                 }
390         }
391         log.Printf("Groups created: %d. Memberships added: %d, removed: %d, skipped: %d", groupsCreated, membershipsAdded, membershipsRemoved, membershipsSkipped)
392
393         return nil
394 }
395
396 // ProcessFile reads the CSV file and process every record
397 func ProcessFile(
398         cfg *ConfigParams,
399         f *os.File,
400         userIDToUUID map[string]string,
401         groupNameToUUID map[string]string,
402         remoteGroups map[string]*GroupInfo,
403         allUsers map[string]arvados.User,
404 ) (groupsCreated, membersAdded, membersSkipped int, err error) {
405         lineNo := 0
406         csvReader := csv.NewReader(f)
407         // Allow variable number of fields.
408         csvReader.FieldsPerRecord = -1
409         for {
410                 record, e := csvReader.Read()
411                 if e == io.EOF {
412                         break
413                 }
414                 lineNo++
415                 if e != nil {
416                         err = fmt.Errorf("error parsing %q, line %d", cfg.Path, lineNo)
417                         return
418                 }
419                 // Only allow 2 or 3 fields per record for backwards compatibility.
420                 if len(record) < 2 || len(record) > 3 {
421                         err = fmt.Errorf("error parsing %q, line %d: found %d fields but only 2 or 3 are allowed", cfg.Path, lineNo, len(record))
422                         return
423                 }
424                 groupName := strings.TrimSpace(record[0])
425                 groupMember := strings.TrimSpace(record[1]) // User ID (username or email)
426                 groupPermission := "can_write"
427                 if len(record) == 3 {
428                         groupPermission = strings.ToLower(record[2])
429                 }
430                 if groupName == "" || groupMember == "" || groupPermission == "" {
431                         log.Printf("Warning: CSV record has at least one empty field (%s, %s, %s). Skipping", groupName, groupMember, groupPermission)
432                         membersSkipped++
433                         continue
434                 }
435                 if cfg.UserID == "username" && cfg.CaseInsensitive {
436                         groupMember = strings.ToLower(groupMember)
437                 }
438                 if !(groupPermission == "can_read" || groupPermission == "can_write" || groupPermission == "can_manage") {
439                         log.Printf("Warning: 3rd field should be 'can_read', 'can_write' or 'can_manage'. Found: %q at line %d, skipping.", groupPermission, lineNo)
440                         membersSkipped++
441                         continue
442                 }
443                 if _, found := userIDToUUID[groupMember]; !found {
444                         // User not present on the system, skip.
445                         log.Printf("Warning: there's no user with %s %q on the system, skipping.", cfg.UserID, groupMember)
446                         membersSkipped++
447                         continue
448                 }
449                 if _, found := groupNameToUUID[groupName]; !found {
450                         // Group doesn't exist, create it before continuing
451                         if cfg.Verbose {
452                                 log.Printf("Remote group %q not found, creating...", groupName)
453                         }
454                         var newGroup arvados.Group
455                         groupData := map[string]string{
456                                 "name":        groupName,
457                                 "owner_uuid":  cfg.ParentGroupUUID,
458                                 "group_class": "role",
459                         }
460                         if e := CreateGroup(cfg, &newGroup, groupData); e != nil {
461                                 err = fmt.Errorf("error creating group named %q: %s", groupName, e)
462                                 return
463                         }
464                         // Update cached group data
465                         groupNameToUUID[groupName] = newGroup.UUID
466                         remoteGroups[newGroup.UUID] = &GroupInfo{
467                                 Group:           newGroup,
468                                 PreviousMembers: make(map[string]GroupPermissions),
469                                 CurrentMembers:  make(map[string]GroupPermissions),
470                         }
471                         groupsCreated++
472                 }
473                 // Both group & user exist, check if user is a member
474                 groupUUID := groupNameToUUID[groupName]
475                 gi := remoteGroups[groupUUID]
476                 if !gi.PreviousMembers[groupMember][groupPermission] && !gi.CurrentMembers[groupMember][groupPermission] {
477                         if cfg.Verbose {
478                                 log.Printf("Adding %q to group %q", groupMember, groupName)
479                         }
480                         // User permissionwasn't there, but should be. Avoid duplicating the
481                         // group->user link when necessary.
482                         createG2ULink := true
483                         if _, ok := gi.PreviousMembers[groupMember]; ok {
484                                 createG2ULink = false // User is already member of the group
485                         }
486                         if e := AddMemberToGroup(cfg, allUsers[userIDToUUID[groupMember]], gi.Group, groupPermission, createG2ULink); e != nil {
487                                 err = e
488                                 return
489                         }
490                         membersAdded++
491                 }
492                 if _, ok := gi.CurrentMembers[groupMember]; ok {
493                         gi.CurrentMembers[groupMember][groupPermission] = true
494                 } else {
495                         gi.CurrentMembers[groupMember] = GroupPermissions{groupPermission: true}
496                 }
497
498         }
499         return
500 }
501
502 // GetAll : Adds all objects of type 'resource' to the 'allItems' list
503 func GetAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
504         // Use the maximum page size the server allows
505         limit := 1<<31 - 1
506         params.Limit = &limit
507         params.Offset = 0
508         params.Order = "uuid"
509         for {
510                 if err = GetResourceList(c, &page, res, params); err != nil {
511                         return allItems, err
512                 }
513                 // Have we finished paging?
514                 if page.Len() == 0 {
515                         break
516                 }
517                 allItems = append(allItems, page.GetItems()...)
518                 params.Offset += page.Len()
519         }
520         return allItems, nil
521 }
522
523 func subtract(setA map[string]GroupPermissions, setB map[string]GroupPermissions) map[string]GroupPermissions {
524         result := make(map[string]GroupPermissions)
525         for element := range setA {
526                 if _, ok := setB[element]; !ok {
527                         result[element] = setA[element]
528                 } else {
529                         for perm := range setA[element] {
530                                 if _, ok := setB[element][perm]; !ok {
531                                         result[element] = GroupPermissions{perm: true}
532                                 }
533                         }
534                 }
535         }
536         return result
537 }
538
539 func jsonReader(rscName string, ob interface{}) io.Reader {
540         j, err := json.Marshal(ob)
541         if err != nil {
542                 panic(err)
543         }
544         v := url.Values{}
545         v[rscName] = []string{string(j)}
546         return bytes.NewBufferString(v.Encode())
547 }
548
549 // GetRemoteGroups fetches all remote groups with their members
550 func GetRemoteGroups(cfg *ConfigParams, allUsers map[string]arvados.User) (remoteGroups map[string]*GroupInfo, groupNameToUUID map[string]string, err error) {
551         remoteGroups = make(map[string]*GroupInfo)
552         groupNameToUUID = make(map[string]string) // Index by group name
553
554         params := arvados.ResourceListParams{
555                 Filters: []arvados.Filter{{
556                         Attr:     "tail_uuid",
557                         Operator: "=",
558                         Operand:  cfg.ParentGroupUUID,
559                 }},
560         }
561         results, err := GetAll(cfg.Client, "links", params, &LinkList{})
562         if err != nil {
563                 return remoteGroups, groupNameToUUID, fmt.Errorf("error getting remote groups: %s", err)
564         }
565         for _, item := range results {
566                 var group arvados.Group
567                 err = GetGroup(cfg, &group, item.(arvados.Link).HeadUUID)
568                 if err != nil {
569                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting remote group: %s", err)
570                 }
571                 // Group -> User filter
572                 g2uFilter := arvados.ResourceListParams{
573                         Filters: []arvados.Filter{{
574                                 Attr:     "owner_uuid",
575                                 Operator: "=",
576                                 Operand:  cfg.SysUserUUID,
577                         }, {
578                                 Attr:     "link_class",
579                                 Operator: "=",
580                                 Operand:  "permission",
581                         }, {
582                                 Attr:     "name",
583                                 Operator: "=",
584                                 Operand:  "can_read",
585                         }, {
586                                 Attr:     "tail_uuid",
587                                 Operator: "=",
588                                 Operand:  group.UUID,
589                         }, {
590                                 Attr:     "head_uuid",
591                                 Operator: "is_a",
592                                 Operand:  "arvados#user",
593                         }},
594                 }
595                 // User -> Group filter
596                 u2gFilter := arvados.ResourceListParams{
597                         Filters: []arvados.Filter{{
598                                 Attr:     "owner_uuid",
599                                 Operator: "=",
600                                 Operand:  cfg.SysUserUUID,
601                         }, {
602                                 Attr:     "link_class",
603                                 Operator: "=",
604                                 Operand:  "permission",
605                         }, {
606                                 Attr:     "name",
607                                 Operator: "in",
608                                 Operand:  []string{"can_read", "can_write", "can_manage"},
609                         }, {
610                                 Attr:     "head_uuid",
611                                 Operator: "=",
612                                 Operand:  group.UUID,
613                         }, {
614                                 Attr:     "tail_uuid",
615                                 Operator: "is_a",
616                                 Operand:  "arvados#user",
617                         }},
618                 }
619                 g2uLinks, err := GetAll(cfg.Client, "links", g2uFilter, &LinkList{})
620                 if err != nil {
621                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting group->user 'can_read' links for group %q: %s", group.Name, err)
622                 }
623                 u2gLinks, err := GetAll(cfg.Client, "links", u2gFilter, &LinkList{})
624                 if err != nil {
625                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting user->group links for group %q: %s", group.Name, err)
626                 }
627                 // Build a list of user ids (email or username) belonging to this group.
628                 membersSet := make(map[string]GroupPermissions)
629                 u2gLinkSet := make(map[string]GroupPermissions)
630                 for _, l := range u2gLinks {
631                         link := l.(arvados.Link)
632                         // Also save the member's group access level.
633                         if _, ok := u2gLinkSet[link.TailUUID]; ok {
634                                 u2gLinkSet[link.TailUUID][link.Name] = true
635                         } else {
636                                 u2gLinkSet[link.TailUUID] = GroupPermissions{link.Name: true}
637                         }
638                 }
639                 for _, item := range g2uLinks {
640                         link := item.(arvados.Link)
641                         // We may have received an old link pointing to a removed account.
642                         if _, found := allUsers[link.HeadUUID]; !found {
643                                 continue
644                         }
645                         // The matching User -> Group link may not exist if the link
646                         // creation failed on a previous run. If that's the case, don't
647                         // include this account on the "previous members" list.
648                         if _, found := u2gLinkSet[link.HeadUUID]; !found {
649                                 continue
650                         }
651                         memberID, err := GetUserID(allUsers[link.HeadUUID], cfg.UserID)
652                         if err != nil {
653                                 return remoteGroups, groupNameToUUID, err
654                         }
655                         if cfg.UserID == "username" && cfg.CaseInsensitive {
656                                 memberID = strings.ToLower(memberID)
657                         }
658                         membersSet[memberID] = u2gLinkSet[link.HeadUUID]
659                 }
660                 remoteGroups[group.UUID] = &GroupInfo{
661                         Group:           group,
662                         PreviousMembers: membersSet,
663                         CurrentMembers:  make(map[string]GroupPermissions),
664                 }
665                 groupNameToUUID[group.Name] = group.UUID
666         }
667         return remoteGroups, groupNameToUUID, nil
668 }
669
670 // RemoveMemberLinksFromGroup remove all links related to the membership
671 func RemoveMemberLinksFromGroup(cfg *ConfigParams, user arvados.User, linkNames []string, completeRemoval bool, group arvados.Group) error {
672         if cfg.Verbose {
673                 log.Printf("Getting group membership links for user %q (%s) on group %q (%s)", user.Username, user.UUID, group.Name, group.UUID)
674         }
675         var links []interface{}
676         var filters [][]arvados.Filter
677         if completeRemoval {
678                 // Search for all group<->user links (both ways)
679                 filters = [][]arvados.Filter{
680                         // Group -> User
681                         {{
682                                 Attr:     "link_class",
683                                 Operator: "=",
684                                 Operand:  "permission",
685                         }, {
686                                 Attr:     "tail_uuid",
687                                 Operator: "=",
688                                 Operand:  group.UUID,
689                         }, {
690                                 Attr:     "head_uuid",
691                                 Operator: "=",
692                                 Operand:  user.UUID,
693                         }},
694                         // Group <- User
695                         {{
696                                 Attr:     "link_class",
697                                 Operator: "=",
698                                 Operand:  "permission",
699                         }, {
700                                 Attr:     "tail_uuid",
701                                 Operator: "=",
702                                 Operand:  user.UUID,
703                         }, {
704                                 Attr:     "head_uuid",
705                                 Operator: "=",
706                                 Operand:  group.UUID,
707                         }},
708                 }
709         } else {
710                 // Search only for the requested Group <- User permission links
711                 filters = [][]arvados.Filter{
712                         {{
713                                 Attr:     "link_class",
714                                 Operator: "=",
715                                 Operand:  "permission",
716                         }, {
717                                 Attr:     "tail_uuid",
718                                 Operator: "=",
719                                 Operand:  user.UUID,
720                         }, {
721                                 Attr:     "head_uuid",
722                                 Operator: "=",
723                                 Operand:  group.UUID,
724                         }, {
725                                 Attr:     "name",
726                                 Operator: "in",
727                                 Operand:  linkNames,
728                         }},
729                 }
730         }
731
732         for _, filterset := range filters {
733                 l, err := GetAll(cfg.Client, "links", arvados.ResourceListParams{Filters: filterset}, &LinkList{})
734                 if err != nil {
735                         userID, _ := GetUserID(user, cfg.UserID)
736                         return fmt.Errorf("error getting links needed to remove user %q from group %q: %s", userID, group.Name, err)
737                 }
738                 links = append(links, l...)
739         }
740         for _, item := range links {
741                 link := item.(arvados.Link)
742                 userID, _ := GetUserID(user, cfg.UserID)
743                 if cfg.Verbose {
744                         log.Printf("Removing %q permission link for %q on group %q", link.Name, userID, group.Name)
745                 }
746                 if err := DeleteLink(cfg, link.UUID); err != nil {
747                         return fmt.Errorf("error removing user %q from group %q: %s", userID, group.Name, err)
748                 }
749         }
750         return nil
751 }
752
753 // AddMemberToGroup create membership links
754 func AddMemberToGroup(cfg *ConfigParams, user arvados.User, group arvados.Group, perm string, createG2ULink bool) error {
755         var newLink arvados.Link
756         var linkData map[string]string
757         if createG2ULink {
758                 linkData = map[string]string{
759                         "owner_uuid": cfg.SysUserUUID,
760                         "link_class": "permission",
761                         "name":       "can_read",
762                         "tail_uuid":  group.UUID,
763                         "head_uuid":  user.UUID,
764                 }
765                 if err := CreateLink(cfg, &newLink, linkData); err != nil {
766                         userID, _ := GetUserID(user, cfg.UserID)
767                         return fmt.Errorf("error adding group %q -> user %q read permission: %s", group.Name, userID, err)
768                 }
769         }
770         linkData = map[string]string{
771                 "owner_uuid": cfg.SysUserUUID,
772                 "link_class": "permission",
773                 "name":       perm,
774                 "tail_uuid":  user.UUID,
775                 "head_uuid":  group.UUID,
776         }
777         if err := CreateLink(cfg, &newLink, linkData); err != nil {
778                 userID, _ := GetUserID(user, cfg.UserID)
779                 return fmt.Errorf("error adding user %q -> group %q %s permission: %s", userID, group.Name, perm, err)
780         }
781         return nil
782 }
783
784 // CreateGroup creates a group with groupData parameters, assigns it to dst
785 func CreateGroup(cfg *ConfigParams, dst *arvados.Group, groupData map[string]string) error {
786         return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/groups", jsonReader("group", groupData), nil)
787 }
788
789 // GetGroup fetches a group by its UUID
790 func GetGroup(cfg *ConfigParams, dst *arvados.Group, groupUUID string) error {
791         return cfg.Client.RequestAndDecode(&dst, "GET", "/arvados/v1/groups/"+groupUUID, nil, nil)
792 }
793
794 // CreateLink creates a link with linkData parameters, assigns it to dst
795 func CreateLink(cfg *ConfigParams, dst *arvados.Link, linkData map[string]string) error {
796         return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/links", jsonReader("link", linkData), nil)
797 }
798
799 // DeleteLink deletes a link by its UUID
800 func DeleteLink(cfg *ConfigParams, linkUUID string) error {
801         if linkUUID == "" {
802                 return fmt.Errorf("cannot delete link with invalid UUID: %q", linkUUID)
803         }
804         return cfg.Client.RequestAndDecode(&arvados.Link{}, "DELETE", "/arvados/v1/links/"+linkUUID, nil, nil)
805 }
806
807 // GetResourceList fetches res list using params
808 func GetResourceList(c *arvados.Client, dst *resourceList, res string, params interface{}) error {
809         return c.RequestAndDecode(dst, "GET", "/arvados/v1/"+res, nil, params)
810 }