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