18713: Merge branch 'main' into 18713-nvidia-persistenced
[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         }
185         srcPath := &os.Args[flags.NFlag()+1]
186
187         // Validations
188         if *srcPath == "" {
189                 return fmt.Errorf("input file path invalid")
190         }
191         if !userIDOpts[*userID] {
192                 var options []string
193                 for opt := range userIDOpts {
194                         options = append(options, opt)
195                 }
196                 return fmt.Errorf("user ID must be one of: %s", strings.Join(options, ", "))
197         }
198
199         config.Path = *srcPath
200         config.ParentGroupUUID = *parentGroupUUID
201         config.UserID = *userID
202         config.Verbose = *verbose
203         config.CaseInsensitive = *caseInsensitive
204
205         return nil
206 }
207
208 // SetParentGroup finds/create parent group of all remote groups
209 func SetParentGroup(cfg *ConfigParams) error {
210         var parentGroup arvados.Group
211         if cfg.ParentGroupUUID == "" {
212                 // UUID not provided, search for preexisting parent group
213                 var gl GroupList
214                 params := arvados.ResourceListParams{
215                         Filters: []arvados.Filter{{
216                                 Attr:     "name",
217                                 Operator: "=",
218                                 Operand:  cfg.ParentGroupName,
219                         }, {
220                                 Attr:     "owner_uuid",
221                                 Operator: "=",
222                                 Operand:  cfg.SysUserUUID,
223                         }},
224                 }
225                 if err := cfg.Client.RequestAndDecode(&gl, "GET", "/arvados/v1/groups", nil, params); err != nil {
226                         return fmt.Errorf("error searching for parent group: %s", err)
227                 }
228                 if len(gl.Items) == 0 {
229                         // Default parent group does not exist, create it.
230                         if cfg.Verbose {
231                                 log.Println("Default parent group not found, creating...")
232                         }
233                         groupData := map[string]string{
234                                 "name":        cfg.ParentGroupName,
235                                 "owner_uuid":  cfg.SysUserUUID,
236                                 "group_class": "role",
237                         }
238                         if err := CreateGroup(cfg, &parentGroup, groupData); err != nil {
239                                 return fmt.Errorf("error creating system user owned group named %q: %s", groupData["name"], err)
240                         }
241                 } else if len(gl.Items) == 1 {
242                         // Default parent group found.
243                         parentGroup = gl.Items[0]
244                 } else {
245                         // This should never happen, as there's an unique index for
246                         // (owner_uuid, name) on groups.
247                         return fmt.Errorf("bug: found %d groups owned by system user and named %q", len(gl.Items), cfg.ParentGroupName)
248                 }
249                 cfg.ParentGroupUUID = parentGroup.UUID
250         } else {
251                 // UUID provided. Check if exists and if it's owned by system user
252                 if err := GetGroup(cfg, &parentGroup, cfg.ParentGroupUUID); err != nil {
253                         return fmt.Errorf("error searching for parent group with UUID %q: %s", cfg.ParentGroupUUID, err)
254                 }
255                 if parentGroup.OwnerUUID != cfg.SysUserUUID {
256                         return fmt.Errorf("parent group %q (%s) must be owned by system user", parentGroup.Name, cfg.ParentGroupUUID)
257                 }
258         }
259         return nil
260 }
261
262 // GetConfig sets up a ConfigParams struct
263 func GetConfig() (config ConfigParams, err error) {
264         config.ParentGroupName = "Externally synchronized groups"
265
266         // Command arguments
267         err = ParseFlags(&config)
268         if err != nil {
269                 return config, err
270         }
271
272         // Arvados Client setup
273         config.Client = arvados.NewClientFromEnv()
274
275         // Check current user permissions & get System user's UUID
276         u, err := config.Client.CurrentUser()
277         if err != nil {
278                 return config, fmt.Errorf("error getting the current user: %s", err)
279         }
280         if !u.IsActive || !u.IsAdmin {
281                 return config, fmt.Errorf("current user (%s) is not an active admin user", u.UUID)
282         }
283
284         var ac struct{ ClusterID string }
285         err = config.Client.RequestAndDecode(&ac, "GET", "arvados/v1/config", nil, nil)
286         if err != nil {
287                 return config, fmt.Errorf("error getting the exported config: %s", err)
288         }
289         config.SysUserUUID = ac.ClusterID + "-tpzed-000000000000000"
290
291         // Set up remote groups' parent
292         if err = SetParentGroup(&config); err != nil {
293                 return config, err
294         }
295
296         return config, nil
297 }
298
299 func doMain(cfg *ConfigParams) error {
300         // Try opening the input file early, just in case there's a problem.
301         f, err := os.Open(cfg.Path)
302         if err != nil {
303                 return fmt.Errorf("%s", err)
304         }
305         defer f.Close()
306
307         iCaseLog := ""
308         if cfg.UserID == "username" && cfg.CaseInsensitive {
309                 iCaseLog = " - username matching requested to be case-insensitive"
310         }
311         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)
312
313         // Get the complete user list to minimize API Server requests
314         allUsers := make(map[string]arvados.User)
315         userIDToUUID := make(map[string]string) // Index by email or username
316         results, err := GetAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
317         if err != nil {
318                 return fmt.Errorf("error getting user list: %s", err)
319         }
320         log.Printf("Found %d users", len(results))
321         for _, item := range results {
322                 u := item.(arvados.User)
323                 allUsers[u.UUID] = u
324                 uID, err := GetUserID(u, cfg.UserID)
325                 if err != nil {
326                         return err
327                 }
328                 if cfg.UserID == "username" && uID != "" && cfg.CaseInsensitive {
329                         uID = strings.ToLower(uID)
330                         if uuid, found := userIDToUUID[uID]; found {
331                                 return fmt.Errorf("case insensitive collision for username %q between %q and %q", uID, u.UUID, uuid)
332                         }
333                 }
334                 userIDToUUID[uID] = u.UUID
335                 if cfg.Verbose {
336                         log.Printf("Seen user %q (%s)", u.Username, u.UUID)
337                 }
338         }
339
340         // Get remote groups and their members
341         remoteGroups, groupNameToUUID, err := GetRemoteGroups(cfg, allUsers)
342         if err != nil {
343                 return err
344         }
345         log.Printf("Found %d remote groups", len(remoteGroups))
346         if cfg.Verbose {
347                 for groupUUID := range remoteGroups {
348                         log.Printf("- Group %q: %d users", remoteGroups[groupUUID].Group.Name, len(remoteGroups[groupUUID].PreviousMembers))
349                 }
350         }
351
352         membershipsRemoved := 0
353
354         // Read the CSV file
355         groupsCreated, membershipsAdded, membershipsSkipped, err := ProcessFile(cfg, f, userIDToUUID, groupNameToUUID, remoteGroups, allUsers)
356         if err != nil {
357                 return err
358         }
359
360         // Remove previous members not listed on this run
361         for groupUUID := range remoteGroups {
362                 gi := remoteGroups[groupUUID]
363                 evictedMemberPerms := subtract(gi.PreviousMembers, gi.CurrentMembers)
364                 groupName := gi.Group.Name
365                 if len(evictedMemberPerms) > 0 {
366                         log.Printf("Removing permissions from %d users on group %q", len(evictedMemberPerms), groupName)
367                 }
368                 for member := range evictedMemberPerms {
369                         var perms []string
370                         completeMembershipRemoval := false
371                         if _, ok := gi.CurrentMembers[member]; !ok {
372                                 completeMembershipRemoval = true
373                                 membershipsRemoved++
374                         } else {
375                                 // Collect which user->group permission links should be removed
376                                 for p := range evictedMemberPerms[member] {
377                                         if evictedMemberPerms[member][p] {
378                                                 perms = append(perms, p)
379                                         }
380                                 }
381                                 membershipsRemoved += len(perms)
382                         }
383                         if err := RemoveMemberLinksFromGroup(cfg, allUsers[userIDToUUID[member]],
384                                 perms, completeMembershipRemoval, gi.Group); err != nil {
385                                 return err
386                         }
387                 }
388         }
389         log.Printf("Groups created: %d. Memberships added: %d, removed: %d, skipped: %d", groupsCreated, membershipsAdded, membershipsRemoved, membershipsSkipped)
390
391         return nil
392 }
393
394 // ProcessFile reads the CSV file and process every record
395 func ProcessFile(
396         cfg *ConfigParams,
397         f *os.File,
398         userIDToUUID map[string]string,
399         groupNameToUUID map[string]string,
400         remoteGroups map[string]*GroupInfo,
401         allUsers map[string]arvados.User,
402 ) (groupsCreated, membersAdded, membersSkipped int, err error) {
403         lineNo := 0
404         csvReader := csv.NewReader(f)
405         // Allow variable number of fields.
406         csvReader.FieldsPerRecord = -1
407         for {
408                 record, e := csvReader.Read()
409                 if e == io.EOF {
410                         break
411                 }
412                 lineNo++
413                 if e != nil {
414                         err = fmt.Errorf("error parsing %q, line %d", cfg.Path, lineNo)
415                         return
416                 }
417                 // Only allow 2 or 3 fields per record for backwards compatibility.
418                 if len(record) < 2 || len(record) > 3 {
419                         err = fmt.Errorf("error parsing %q, line %d: found %d fields but only 2 or 3 are allowed", cfg.Path, lineNo, len(record))
420                         return
421                 }
422                 groupName := strings.TrimSpace(record[0])
423                 groupMember := strings.TrimSpace(record[1]) // User ID (username or email)
424                 groupPermission := "can_write"
425                 if len(record) == 3 {
426                         groupPermission = strings.ToLower(record[2])
427                 }
428                 if groupName == "" || groupMember == "" || groupPermission == "" {
429                         log.Printf("Warning: CSV record has at least one empty field (%s, %s, %s). Skipping", groupName, groupMember, groupPermission)
430                         membersSkipped++
431                         continue
432                 }
433                 if cfg.UserID == "username" && cfg.CaseInsensitive {
434                         groupMember = strings.ToLower(groupMember)
435                 }
436                 if !(groupPermission == "can_read" || groupPermission == "can_write" || groupPermission == "can_manage") {
437                         log.Printf("Warning: 3rd field should be 'can_read', 'can_write' or 'can_manage'. Found: %q at line %d, skipping.", groupPermission, lineNo)
438                         membersSkipped++
439                         continue
440                 }
441                 if _, found := userIDToUUID[groupMember]; !found {
442                         // User not present on the system, skip.
443                         log.Printf("Warning: there's no user with %s %q on the system, skipping.", cfg.UserID, groupMember)
444                         membersSkipped++
445                         continue
446                 }
447                 if _, found := groupNameToUUID[groupName]; !found {
448                         // Group doesn't exist, create it before continuing
449                         if cfg.Verbose {
450                                 log.Printf("Remote group %q not found, creating...", groupName)
451                         }
452                         var newGroup arvados.Group
453                         groupData := map[string]string{
454                                 "name":        groupName,
455                                 "owner_uuid":  cfg.ParentGroupUUID,
456                                 "group_class": "role",
457                         }
458                         if e := CreateGroup(cfg, &newGroup, groupData); e != nil {
459                                 err = fmt.Errorf("error creating group named %q: %s", groupName, e)
460                                 return
461                         }
462                         // Update cached group data
463                         groupNameToUUID[groupName] = newGroup.UUID
464                         remoteGroups[newGroup.UUID] = &GroupInfo{
465                                 Group:           newGroup,
466                                 PreviousMembers: make(map[string]GroupPermissions),
467                                 CurrentMembers:  make(map[string]GroupPermissions),
468                         }
469                         groupsCreated++
470                 }
471                 // Both group & user exist, check if user is a member
472                 groupUUID := groupNameToUUID[groupName]
473                 gi := remoteGroups[groupUUID]
474                 if !gi.PreviousMembers[groupMember][groupPermission] && !gi.CurrentMembers[groupMember][groupPermission] {
475                         if cfg.Verbose {
476                                 log.Printf("Adding %q to group %q", groupMember, groupName)
477                         }
478                         // User permissionwasn't there, but should be. Avoid duplicating the
479                         // group->user link when necessary.
480                         createG2ULink := true
481                         if _, ok := gi.PreviousMembers[groupMember]; ok {
482                                 createG2ULink = false // User is already member of the group
483                         }
484                         if e := AddMemberToGroup(cfg, allUsers[userIDToUUID[groupMember]], gi.Group, groupPermission, createG2ULink); e != nil {
485                                 err = e
486                                 return
487                         }
488                         membersAdded++
489                 }
490                 if _, ok := gi.CurrentMembers[groupMember]; ok {
491                         gi.CurrentMembers[groupMember][groupPermission] = true
492                 } else {
493                         gi.CurrentMembers[groupMember] = GroupPermissions{groupPermission: true}
494                 }
495
496         }
497         return
498 }
499
500 // GetAll : Adds all objects of type 'resource' to the 'allItems' list
501 func GetAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
502         // Use the maximum page size the server allows
503         limit := 1<<31 - 1
504         params.Limit = &limit
505         params.Offset = 0
506         params.Order = "uuid"
507         for {
508                 if err = GetResourceList(c, &page, res, params); err != nil {
509                         return allItems, err
510                 }
511                 // Have we finished paging?
512                 if page.Len() == 0 {
513                         break
514                 }
515                 allItems = append(allItems, page.GetItems()...)
516                 params.Offset += page.Len()
517         }
518         return allItems, nil
519 }
520
521 func subtract(setA map[string]GroupPermissions, setB map[string]GroupPermissions) map[string]GroupPermissions {
522         result := make(map[string]GroupPermissions)
523         for element := range setA {
524                 if _, ok := setB[element]; !ok {
525                         result[element] = setA[element]
526                 } else {
527                         for perm := range setA[element] {
528                                 if _, ok := setB[element][perm]; !ok {
529                                         result[element] = GroupPermissions{perm: true}
530                                 }
531                         }
532                 }
533         }
534         return result
535 }
536
537 func jsonReader(rscName string, ob interface{}) io.Reader {
538         j, err := json.Marshal(ob)
539         if err != nil {
540                 panic(err)
541         }
542         v := url.Values{}
543         v[rscName] = []string{string(j)}
544         return bytes.NewBufferString(v.Encode())
545 }
546
547 // GetRemoteGroups fetches all remote groups with their members
548 func GetRemoteGroups(cfg *ConfigParams, allUsers map[string]arvados.User) (remoteGroups map[string]*GroupInfo, groupNameToUUID map[string]string, err error) {
549         remoteGroups = make(map[string]*GroupInfo)
550         groupNameToUUID = make(map[string]string) // Index by group name
551
552         params := arvados.ResourceListParams{
553                 Filters: []arvados.Filter{{
554                         Attr:     "tail_uuid",
555                         Operator: "=",
556                         Operand:  cfg.ParentGroupUUID,
557                 }},
558         }
559         results, err := GetAll(cfg.Client, "links", params, &LinkList{})
560         if err != nil {
561                 return remoteGroups, groupNameToUUID, fmt.Errorf("error getting remote groups: %s", err)
562         }
563         for _, item := range results {
564                 var group arvados.Group
565                 err = GetGroup(cfg, &group, item.(arvados.Link).HeadUUID)
566                 if err != nil {
567                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting remote group: %s", err)
568                 }
569                 // Group -> User filter
570                 g2uFilter := arvados.ResourceListParams{
571                         Filters: []arvados.Filter{{
572                                 Attr:     "owner_uuid",
573                                 Operator: "=",
574                                 Operand:  cfg.SysUserUUID,
575                         }, {
576                                 Attr:     "link_class",
577                                 Operator: "=",
578                                 Operand:  "permission",
579                         }, {
580                                 Attr:     "name",
581                                 Operator: "=",
582                                 Operand:  "can_read",
583                         }, {
584                                 Attr:     "tail_uuid",
585                                 Operator: "=",
586                                 Operand:  group.UUID,
587                         }, {
588                                 Attr:     "head_uuid",
589                                 Operator: "is_a",
590                                 Operand:  "arvados#user",
591                         }},
592                 }
593                 // User -> Group filter
594                 u2gFilter := arvados.ResourceListParams{
595                         Filters: []arvados.Filter{{
596                                 Attr:     "owner_uuid",
597                                 Operator: "=",
598                                 Operand:  cfg.SysUserUUID,
599                         }, {
600                                 Attr:     "link_class",
601                                 Operator: "=",
602                                 Operand:  "permission",
603                         }, {
604                                 Attr:     "name",
605                                 Operator: "in",
606                                 Operand:  []string{"can_read", "can_write", "can_manage"},
607                         }, {
608                                 Attr:     "head_uuid",
609                                 Operator: "=",
610                                 Operand:  group.UUID,
611                         }, {
612                                 Attr:     "tail_uuid",
613                                 Operator: "is_a",
614                                 Operand:  "arvados#user",
615                         }},
616                 }
617                 g2uLinks, err := GetAll(cfg.Client, "links", g2uFilter, &LinkList{})
618                 if err != nil {
619                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting group->user 'can_read' links for group %q: %s", group.Name, err)
620                 }
621                 u2gLinks, err := GetAll(cfg.Client, "links", u2gFilter, &LinkList{})
622                 if err != nil {
623                         return remoteGroups, groupNameToUUID, fmt.Errorf("error getting user->group links for group %q: %s", group.Name, err)
624                 }
625                 // Build a list of user ids (email or username) belonging to this group.
626                 membersSet := make(map[string]GroupPermissions)
627                 u2gLinkSet := make(map[string]GroupPermissions)
628                 for _, l := range u2gLinks {
629                         link := l.(arvados.Link)
630                         // Also save the member's group access level.
631                         if _, ok := u2gLinkSet[link.TailUUID]; ok {
632                                 u2gLinkSet[link.TailUUID][link.Name] = true
633                         } else {
634                                 u2gLinkSet[link.TailUUID] = GroupPermissions{link.Name: true}
635                         }
636                 }
637                 for _, item := range g2uLinks {
638                         link := item.(arvados.Link)
639                         // We may have received an old link pointing to a removed account.
640                         if _, found := allUsers[link.HeadUUID]; !found {
641                                 continue
642                         }
643                         // The matching User -> Group link may not exist if the link
644                         // creation failed on a previous run. If that's the case, don't
645                         // include this account on the "previous members" list.
646                         if _, found := u2gLinkSet[link.HeadUUID]; !found {
647                                 continue
648                         }
649                         memberID, err := GetUserID(allUsers[link.HeadUUID], cfg.UserID)
650                         if err != nil {
651                                 return remoteGroups, groupNameToUUID, err
652                         }
653                         if cfg.UserID == "username" && cfg.CaseInsensitive {
654                                 memberID = strings.ToLower(memberID)
655                         }
656                         membersSet[memberID] = u2gLinkSet[link.HeadUUID]
657                 }
658                 remoteGroups[group.UUID] = &GroupInfo{
659                         Group:           group,
660                         PreviousMembers: membersSet,
661                         CurrentMembers:  make(map[string]GroupPermissions),
662                 }
663                 groupNameToUUID[group.Name] = group.UUID
664         }
665         return remoteGroups, groupNameToUUID, nil
666 }
667
668 // RemoveMemberLinksFromGroup remove all links related to the membership
669 func RemoveMemberLinksFromGroup(cfg *ConfigParams, user arvados.User, linkNames []string, completeRemoval bool, group arvados.Group) error {
670         if cfg.Verbose {
671                 log.Printf("Getting group membership links for user %q (%s) on group %q (%s)", user.Username, user.UUID, group.Name, group.UUID)
672         }
673         var links []interface{}
674         var filters [][]arvados.Filter
675         if completeRemoval {
676                 // Search for all group<->user links (both ways)
677                 filters = [][]arvados.Filter{
678                         // Group -> User
679                         {{
680                                 Attr:     "link_class",
681                                 Operator: "=",
682                                 Operand:  "permission",
683                         }, {
684                                 Attr:     "tail_uuid",
685                                 Operator: "=",
686                                 Operand:  group.UUID,
687                         }, {
688                                 Attr:     "head_uuid",
689                                 Operator: "=",
690                                 Operand:  user.UUID,
691                         }},
692                         // Group <- User
693                         {{
694                                 Attr:     "link_class",
695                                 Operator: "=",
696                                 Operand:  "permission",
697                         }, {
698                                 Attr:     "tail_uuid",
699                                 Operator: "=",
700                                 Operand:  user.UUID,
701                         }, {
702                                 Attr:     "head_uuid",
703                                 Operator: "=",
704                                 Operand:  group.UUID,
705                         }},
706                 }
707         } else {
708                 // Search only for the requested Group <- User permission links
709                 filters = [][]arvados.Filter{
710                         {{
711                                 Attr:     "link_class",
712                                 Operator: "=",
713                                 Operand:  "permission",
714                         }, {
715                                 Attr:     "tail_uuid",
716                                 Operator: "=",
717                                 Operand:  user.UUID,
718                         }, {
719                                 Attr:     "head_uuid",
720                                 Operator: "=",
721                                 Operand:  group.UUID,
722                         }, {
723                                 Attr:     "name",
724                                 Operator: "in",
725                                 Operand:  linkNames,
726                         }},
727                 }
728         }
729
730         for _, filterset := range filters {
731                 l, err := GetAll(cfg.Client, "links", arvados.ResourceListParams{Filters: filterset}, &LinkList{})
732                 if err != nil {
733                         userID, _ := GetUserID(user, cfg.UserID)
734                         return fmt.Errorf("error getting links needed to remove user %q from group %q: %s", userID, group.Name, err)
735                 }
736                 links = append(links, l...)
737         }
738         for _, item := range links {
739                 link := item.(arvados.Link)
740                 userID, _ := GetUserID(user, cfg.UserID)
741                 if cfg.Verbose {
742                         log.Printf("Removing %q permission link for %q on group %q", link.Name, userID, group.Name)
743                 }
744                 if err := DeleteLink(cfg, link.UUID); err != nil {
745                         return fmt.Errorf("error removing user %q from group %q: %s", userID, group.Name, err)
746                 }
747         }
748         return nil
749 }
750
751 // AddMemberToGroup create membership links
752 func AddMemberToGroup(cfg *ConfigParams, user arvados.User, group arvados.Group, perm string, createG2ULink bool) error {
753         var newLink arvados.Link
754         var linkData map[string]string
755         if createG2ULink {
756                 linkData = map[string]string{
757                         "owner_uuid": cfg.SysUserUUID,
758                         "link_class": "permission",
759                         "name":       "can_read",
760                         "tail_uuid":  group.UUID,
761                         "head_uuid":  user.UUID,
762                 }
763                 if err := CreateLink(cfg, &newLink, linkData); err != nil {
764                         userID, _ := GetUserID(user, cfg.UserID)
765                         return fmt.Errorf("error adding group %q -> user %q read permission: %s", group.Name, userID, err)
766                 }
767         }
768         linkData = map[string]string{
769                 "owner_uuid": cfg.SysUserUUID,
770                 "link_class": "permission",
771                 "name":       perm,
772                 "tail_uuid":  user.UUID,
773                 "head_uuid":  group.UUID,
774         }
775         if err := CreateLink(cfg, &newLink, linkData); err != nil {
776                 userID, _ := GetUserID(user, cfg.UserID)
777                 return fmt.Errorf("error adding user %q -> group %q %s permission: %s", userID, group.Name, perm, err)
778         }
779         return nil
780 }
781
782 // CreateGroup creates a group with groupData parameters, assigns it to dst
783 func CreateGroup(cfg *ConfigParams, dst *arvados.Group, groupData map[string]string) error {
784         return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/groups", jsonReader("group", groupData), nil)
785 }
786
787 // GetGroup fetches a group by its UUID
788 func GetGroup(cfg *ConfigParams, dst *arvados.Group, groupUUID string) error {
789         return cfg.Client.RequestAndDecode(&dst, "GET", "/arvados/v1/groups/"+groupUUID, nil, nil)
790 }
791
792 // CreateLink creates a link with linkData parameters, assigns it to dst
793 func CreateLink(cfg *ConfigParams, dst *arvados.Link, linkData map[string]string) error {
794         return cfg.Client.RequestAndDecode(dst, "POST", "/arvados/v1/links", jsonReader("link", linkData), nil)
795 }
796
797 // DeleteLink deletes a link by its UUID
798 func DeleteLink(cfg *ConfigParams, linkUUID string) error {
799         if linkUUID == "" {
800                 return fmt.Errorf("cannot delete link with invalid UUID: %q", linkUUID)
801         }
802         return cfg.Client.RequestAndDecode(&arvados.Link{}, "DELETE", "/arvados/v1/links/"+linkUUID, nil, nil)
803 }
804
805 // GetResourceList fetches res list using params
806 func GetResourceList(c *arvados.Client, dst *resourceList, res string, params interface{}) error {
807         return c.RequestAndDecode(dst, "GET", "/arvados/v1/"+res, nil, params)
808 }