From ca50b1e9081c41ec4707b21cae7294eb89342489 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Mon, 8 Jan 2024 19:14:25 +0530 Subject: [PATCH] Filter out non existent entities in permissions and link shares (#4955) --- #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No #### Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Issue(s) * # #### Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- .../m365/collection/drive/permission.go | 100 ++++++- .../m365/collection/drive/permission_test.go | 258 ++++++++++++++++++ .../m365/collection/drive/restore_caches.go | 26 ++ .../m365/collection/drive/restore_test.go | 63 ++++- src/internal/m365/collection/site/restore.go | 2 +- src/internal/m365/service/groups/restore.go | 2 +- src/internal/m365/service/onedrive/restore.go | 2 +- .../m365/service/sharepoint/restore.go | 2 +- src/pkg/services/m365/api/groups.go | 20 ++ 9 files changed, 467 insertions(+), 8 deletions(-) diff --git a/src/internal/m365/collection/drive/permission.go b/src/internal/m365/collection/drive/permission.go index 61bdb1955..4430695e8 100644 --- a/src/internal/m365/collection/drive/permission.go +++ b/src/internal/m365/collection/drive/permission.go @@ -397,7 +397,7 @@ func UpdateLinkShares( newLS, err := upils.PostItemLinkShareUpdate(ictx, driveID, itemID, lsbody) if graph.IsErrUsersCannotBeResolved(err) { - oldLinkShareIDToNewID.Store(ls.ID, "") // empty to signify that we could not restore + oldLinkShareIDToNewID.Store(ls.ID, nonRestorablePermission) logger.CtxErr(ictx, err).Info("unable to restore link share") continue @@ -449,6 +449,102 @@ func UpdateLinkShares( return alreadyDeleted, nil } +func filterUnavailableEntitiesInLinkShare( + ctx context.Context, + linkShares []metadata.LinkShare, + availableEntities ResourceIDNames, + oldLinkShareIDToNewID syncd.MapTo[string], +) []metadata.LinkShare { + filtered := []metadata.LinkShare{} + + if availableEntities.Users == nil || availableEntities.Groups == nil { + // This should not be happening unless we missed to fill in the caches + logger.Ctx(ctx).Info("no available entities, not filtering link shares") + return linkShares + } + + for _, p := range linkShares { + entities := []metadata.Entity{} + + for _, e := range p.Entities { + available := false + + switch e.EntityType { + case metadata.GV2User: + _, ok := availableEntities.Users.NameOf(e.ID) + available = available || ok + case metadata.GV2Group: + _, ok := availableEntities.Groups.NameOf(e.ID) + available = available || ok + default: + // We only know about users and groups + available = true + } + + if available { + entities = append(entities, e) + } + } + + if len(entities) > 0 { + p.Entities = entities + filtered = append(filtered, p) + + continue + } + + // If we have no entities, we can't restore the link share + // and so we have to mark it as not restored. + // This is done to make sure we don't try to delete it later. + oldLinkShareIDToNewID.Store(p.ID, "") + } + + return filtered +} + +func filterUnavailableEntitiesInPermissions( + ctx context.Context, + perms []metadata.Permission, + availableEntities ResourceIDNames, + oldPermIDToNewID syncd.MapTo[string], +) []metadata.Permission { + if availableEntities.Users == nil || availableEntities.Groups == nil { + // This should not be happening unless we missed to fill in the caches + logger.Ctx(ctx).Info("no available entities, not filtering link shares") + return perms + } + + filtered := []metadata.Permission{} + + for _, p := range perms { + available := false + + switch p.EntityType { + case metadata.GV2User: + _, ok := availableEntities.Users.NameOf(p.EntityID) + available = available || ok + case metadata.GV2Group: + _, ok := availableEntities.Groups.NameOf(p.EntityID) + available = available || ok + default: + // We only know about users and groups + // TODO: extend the check to other entity types + available = true + } + + if available { + filtered = append(filtered, p) + continue + } + + // If we have no entities, we can't restore the permission + // and so we have to mark it as not restored. + oldPermIDToNewID.Store(p.ID, "") + } + + return filtered +} + // RestorePermissions takes in the permissions of an item, computes // what permissions need to added and removed based on the parent // folder metas and uses that to add/remove the necessary permissions @@ -478,6 +574,7 @@ func RestorePermissions( if previousLinkShares != nil { lsAdded, lsRemoved := metadata.DiffLinkShares(previousLinkShares, current.LinkShares) + lsAdded = filterUnavailableEntitiesInLinkShare(ctx, lsAdded, caches.AvailableEntities, caches.OldLinkShareIDToNewID) // Link shares have to be updated before permissions as we have to // use the information about if we had to reset the inheritance to @@ -503,6 +600,7 @@ func RestorePermissions( } permAdded, permRemoved := metadata.DiffPermissions(previous.Permissions, current.Permissions) + permAdded = filterUnavailableEntitiesInPermissions(ctx, permAdded, caches.AvailableEntities, caches.OldPermIDToNewID) if didReset { // In case we did a reset of permissions when restoring link diff --git a/src/internal/m365/collection/drive/permission_test.go b/src/internal/m365/collection/drive/permission_test.go index d787ae63a..5b2447cb1 100644 --- a/src/internal/m365/collection/drive/permission_test.go +++ b/src/internal/m365/collection/drive/permission_test.go @@ -2,6 +2,7 @@ package drive import ( "context" + "fmt" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/syncd" "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata" @@ -326,3 +328,259 @@ func (suite *PermissionsUnitTestSuite) TestLinkShareRestoreNonExistentUser() { expectedSuccessValues := []bool{true, false, false, false} assert.Equal(t, expectedSuccessValues, successValues) } + +func (suite *PermissionsUnitTestSuite) TestFilterUnavailableEntitiesInPermissions() { + table := []struct { + name string + permissions []metadata.Permission + expected []metadata.Permission + availableEntities ResourceIDNames + skippedPermissions []string + }{ + { + name: "single item", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + }, + expected: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + }, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{"e1": "e1"}), + Groups: idname.NewCache(map[string]string{}), + }, + }, + { + name: "single item with missing entity", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + }, + expected: []metadata.Permission{}, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{}), + Groups: idname.NewCache(map[string]string{}), + }, + skippedPermissions: []string{"p1"}, + }, + { + name: "multiple items", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2User}, + }, + expected: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2User}, + }, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{"e1": "e1", "e2": "e2", "e3": "e3"}), + Groups: idname.NewCache(map[string]string{}), + }, + }, + { + name: "multiple items with missing entity", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2Group}, + }, + expected: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2Group}, + }, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{"e1": "e1"}), + Groups: idname.NewCache(map[string]string{"e3": "e3"}), + }, + skippedPermissions: []string{"p2"}, + }, + { + name: "multiple items with missing entity and multiple types", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2Group}, + {ID: "p4", EntityID: "e4", EntityType: metadata.GV2Group}, + {ID: "p5", EntityID: "e5", EntityType: metadata.GV2User}, + }, + expected: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2User}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2Group}, + {ID: "p5", EntityID: "e5", EntityType: metadata.GV2User}, + }, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{"e1": "e1", "e5": "e5"}), + Groups: idname.NewCache(map[string]string{"e3": "e3"}), + }, + skippedPermissions: []string{"p2", "p4"}, + }, + { + name: "single item different type", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2Group}, + }, + expected: []metadata.Permission{}, + availableEntities: ResourceIDNames{ + Users: idname.NewCache(map[string]string{"e1": "e1"}), + Groups: idname.NewCache(map[string]string{}), + }, + skippedPermissions: []string{"p1"}, + }, + { + name: "unhandled types", + permissions: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2Device}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2App}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2SiteUser}, + {ID: "p4", EntityID: "e4", EntityType: metadata.GV2SiteGroup}, + }, + expected: []metadata.Permission{ + {ID: "p1", EntityID: "e1", EntityType: metadata.GV2Device}, + {ID: "p2", EntityID: "e2", EntityType: metadata.GV2App}, + {ID: "p3", EntityID: "e3", EntityType: metadata.GV2SiteUser}, + {ID: "p4", EntityID: "e4", EntityType: metadata.GV2SiteGroup}, + }, + availableEntities: ResourceIDNames{ + // these are users and not what we have + Users: idname.NewCache(map[string]string{"e1": "e1", "e2": "e2", "e3": "e3", "e4": "e4"}), + Groups: idname.NewCache(map[string]string{}), + }, + skippedPermissions: []string{}, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + oldToNew := syncd.NewMapTo[string]() + filtered := filterUnavailableEntitiesInPermissions(ctx, test.permissions, test.availableEntities, oldToNew) + + assert.Equal(t, test.expected, filtered, "filtered permissions") + + for _, id := range test.skippedPermissions { + _, ok := oldToNew.Load(id) + assert.True(t, ok, fmt.Sprintf("skipped id %s", id)) + } + }) + } +} + +type eidtype struct { + id string + etype metadata.GV2Type +} + +func (suite *PermissionsUnitTestSuite) TestFilterUnavailableEntitiesInLinkShare() { + ls := func(lsid string, entities []eidtype) metadata.LinkShare { + ents := []metadata.Entity{} + for _, e := range entities { + ents = append(ents, metadata.Entity{ID: e.id, EntityType: e.etype}) + } + + return metadata.LinkShare{ + ID: lsid, + Entities: ents, + } + } + + ae := func(uids, gids []string) ResourceIDNames { + users := map[string]string{} + for _, id := range uids { + users[id] = id + } + + groups := map[string]string{} + for _, id := range gids { + groups[id] = id + } + + return ResourceIDNames{ + Users: idname.NewCache(users), + Groups: idname.NewCache(groups), + } + } + + table := []struct { + name string + linkShares []metadata.LinkShare + expected []metadata.LinkShare + availableEntities ResourceIDNames + skippedLinkShares []string + }{ + { + name: "single item, single available entity", + linkShares: []metadata.LinkShare{ls("ls1", []eidtype{{"e1", metadata.GV2User}})}, + expected: []metadata.LinkShare{ls("ls1", []eidtype{{"e1", metadata.GV2User}})}, + availableEntities: ae([]string{"e1"}, []string{}), + }, + { + name: "single item, single missing entity", + linkShares: []metadata.LinkShare{ls("ls1", []eidtype{{"e1", metadata.GV2User}})}, + expected: []metadata.LinkShare{}, + availableEntities: ae([]string{}, []string{}), + skippedLinkShares: []string{"ls1"}, + }, + { + name: "multiple items, multiple available entities", + linkShares: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2User}, {"e2", metadata.GV2User}}), + ls("ls2", []eidtype{{"e3", metadata.GV2User}, {"e4", metadata.GV2User}}), + }, + expected: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2User}, {"e2", metadata.GV2User}}), + ls("ls2", []eidtype{{"e3", metadata.GV2User}, {"e4", metadata.GV2User}}), + }, + availableEntities: ae([]string{"e1", "e2", "e3", "e4"}, []string{}), + }, + { + name: "multiple items, missing entities", + linkShares: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2User}, {"e2", metadata.GV2Group}}), + ls("ls2", []eidtype{{"e3", metadata.GV2User}, {"e4", metadata.GV2Group}}), + }, + expected: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2User}}), + ls("ls2", []eidtype{{"e4", metadata.GV2Group}}), + }, + availableEntities: ae([]string{"e1"}, []string{"e4"}), + skippedLinkShares: []string{}, + }, + { + name: "unhandled items", + linkShares: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2Device}, {"e2", metadata.GV2App}}), + ls("ls2", []eidtype{{"e3", metadata.GV2SiteUser}, {"e4", metadata.GV2SiteGroup}}), + }, + expected: []metadata.LinkShare{ + ls("ls1", []eidtype{{"e1", metadata.GV2Device}, {"e2", metadata.GV2App}}), + ls("ls2", []eidtype{{"e3", metadata.GV2SiteUser}, {"e4", metadata.GV2SiteGroup}}), + }, + availableEntities: ae([]string{}, []string{}), + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + oldToNew := syncd.NewMapTo[string]() + filtered := filterUnavailableEntitiesInLinkShare(ctx, test.linkShares, test.availableEntities, oldToNew) + + assert.Equal(t, test.expected, filtered, "filtered link shares") + + for _, id := range test.skippedLinkShares { + _, ok := oldToNew.Load(id) + assert.True(t, ok, fmt.Sprintf("skipped id %s", id)) + } + }) + } +} diff --git a/src/internal/m365/collection/drive/restore_caches.go b/src/internal/m365/collection/drive/restore_caches.go index 621a2a32d..d077c9adc 100644 --- a/src/internal/m365/collection/drive/restore_caches.go +++ b/src/internal/m365/collection/drive/restore_caches.go @@ -11,6 +11,7 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/syncd" "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata" + "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api/graph" ) @@ -21,6 +22,11 @@ type driveInfo struct { rootFolderID string } +type ResourceIDNames struct { + Users idname.Cacher + Groups idname.Cacher +} + type restoreCaches struct { BackupDriveIDName idname.Cacher collisionKeyToItemID map[string]api.DriveItemIDType @@ -30,6 +36,7 @@ type restoreCaches struct { OldLinkShareIDToNewID syncd.MapTo[string] OldPermIDToNewID syncd.MapTo[string] ParentDirToMeta syncd.MapTo[metadata.Metadata] + AvailableEntities ResourceIDNames pool sync.Pool } @@ -59,12 +66,18 @@ func (rc *restoreCaches) AddDrive( return nil } +type GetAllIDsAndNameser interface { + GetAllIDsAndNames(ctx context.Context, errs *fault.Bus) (idname.Cacher, error) +} + // Populate looks up drive items available to the protectedResource // and adds their info to the caches. func (rc *restoreCaches) Populate( ctx context.Context, + usersGetter, groupsGetter GetAllIDsAndNameser, gdparf GetDrivePagerAndRootFolderer, protectedResourceID string, + errs *fault.Bus, ) error { drives, err := api.GetAllDrives( ctx, @@ -79,6 +92,19 @@ func (rc *restoreCaches) Populate( } } + users, err := usersGetter.GetAllIDsAndNames(ctx, errs) + if err != nil { + return clues.Wrap(err, "getting users") + } + + groups, err := groupsGetter.GetAllIDsAndNames(ctx, errs) + if err != nil { + return clues.Wrap(err, "getting groups") + } + + rc.AvailableEntities.Users = users + rc.AvailableEntities.Groups = groups + return nil } diff --git a/src/internal/m365/collection/drive/restore_test.go b/src/internal/m365/collection/drive/restore_test.go index c186ef483..82b84ee3d 100644 --- a/src/internal/m365/collection/drive/restore_test.go +++ b/src/internal/m365/collection/drive/restore_test.go @@ -428,6 +428,17 @@ func (m *mockGDPARF) NewDrivePager( return m.pager } +type mockAllIDsAndNamesGetter struct { + kvs map[string]string +} + +func (m mockAllIDsAndNamesGetter) GetAllIDsAndNames( + context.Context, + *fault.Bus, +) (idname.Cacher, error) { + return idname.NewCache(m.kvs), nil +} + func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { rfID := "this-is-id" driveID := "another-id" @@ -443,12 +454,14 @@ func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { table := []struct { name string mock *apiMock.Pager[models.Driveable] + users map[string]string + groups map[string]string expectErr require.ErrorAssertionFunc expectLen int checkValues bool }{ { - name: "no results", + name: "no drive results", mock: &apiMock.Pager[models.Driveable]{ ToReturn: []apiMock.PagerResult[models.Driveable]{ {Values: []models.Driveable{}}, @@ -458,7 +471,7 @@ func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { expectLen: 0, }, { - name: "one result", + name: "one drive result", mock: &apiMock.Pager[models.Driveable]{ ToReturn: []apiMock.PagerResult[models.Driveable]{ {Values: []models.Driveable{md}}, @@ -478,6 +491,32 @@ func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { expectErr: require.Error, expectLen: 0, }, + { + name: "multiple users", + mock: &apiMock.Pager[models.Driveable]{ + ToReturn: []apiMock.PagerResult[models.Driveable]{ + {Values: []models.Driveable{}}, + }, + }, + users: map[string]string{ + "1": "one", + "2": "two", + }, + expectErr: require.NoError, + }, + { + name: "multiple groups", + mock: &apiMock.Pager[models.Driveable]{ + ToReturn: []apiMock.PagerResult[models.Driveable]{ + {Values: []models.Driveable{}}, + }, + }, + groups: map[string]string{ + "1": "one", + "2": "two", + }, + expectErr: require.NoError, + }, } for _, test := range table { suite.Run(test.name, func() { @@ -491,8 +530,16 @@ func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { pager: test.mock, } + errs := fault.New(true) + rc := NewRestoreCaches(nil) - err := rc.Populate(ctx, gdparf, "shmoo") + err := rc.Populate( + ctx, + mockAllIDsAndNamesGetter{test.users}, + mockAllIDsAndNamesGetter{test.groups}, + gdparf, + "shmoo", + errs) test.expectErr(t, err, clues.ToCore(err)) assert.Equal(t, rc.DriveIDToDriveInfo.Size(), test.expectLen) @@ -509,6 +556,16 @@ func (suite *RestoreUnitSuite) TestRestoreCaches_Populate() { assert.Equal(t, name, nameResult.name, "drive name") assert.Equal(t, rfID, nameResult.rootFolderID, "root folder id") } + + for key, val := range test.users { + name, _ := rc.AvailableEntities.Users.NameOf(key) + assert.Equal(t, name, val, "user") + } + + for key, val := range test.groups { + name, _ := rc.AvailableEntities.Groups.NameOf(key) + assert.Equal(t, name, val, "group") + } }) } } diff --git a/src/internal/m365/collection/site/restore.go b/src/internal/m365/collection/site/restore.go index d4bc8f412..eafd47d65 100644 --- a/src/internal/m365/collection/site/restore.go +++ b/src/internal/m365/collection/site/restore.go @@ -46,7 +46,7 @@ func ConsumeRestoreCollections( el = errs.Local() ) - err := caches.Populate(ctx, lrh, rcc.ProtectedResource.ID()) + err := caches.Populate(ctx, ac.Users(), ac.Groups(), lrh, rcc.ProtectedResource.ID(), errs) if err != nil { return nil, clues.Wrap(err, "initializing restore caches") } diff --git a/src/internal/m365/service/groups/restore.go b/src/internal/m365/service/groups/restore.go index df94f4b20..33ab6e830 100644 --- a/src/internal/m365/service/groups/restore.go +++ b/src/internal/m365/service/groups/restore.go @@ -109,7 +109,7 @@ func (h *groupsHandler) ConsumeRestoreCollections( Selector: rcc.Selector, } - err = caches.Populate(ictx, lrh, srcc.ProtectedResource.ID()) + err = caches.Populate(ictx, h.apiClient.Users(), h.apiClient.Groups(), lrh, srcc.ProtectedResource.ID(), errs) if err != nil { return nil, nil, clues.Wrap(err, "initializing restore caches") } diff --git a/src/internal/m365/service/onedrive/restore.go b/src/internal/m365/service/onedrive/restore.go index 6c8c9a100..8aa1ddf90 100644 --- a/src/internal/m365/service/onedrive/restore.go +++ b/src/internal/m365/service/onedrive/restore.go @@ -53,7 +53,7 @@ func (h *onedriveHandler) ConsumeRestoreCollections( ctx = clues.Add(ctx, "backup_version", rcc.BackupVersion) - err := caches.Populate(ctx, rh, rcc.ProtectedResource.ID()) + err := caches.Populate(ctx, h.apiClient.Users(), h.apiClient.Groups(), rh, rcc.ProtectedResource.ID(), errs) if err != nil { return nil, nil, clues.Wrap(err, "initializing restore caches") } diff --git a/src/internal/m365/service/sharepoint/restore.go b/src/internal/m365/service/sharepoint/restore.go index bd64b707b..1568c4c5f 100644 --- a/src/internal/m365/service/sharepoint/restore.go +++ b/src/internal/m365/service/sharepoint/restore.go @@ -81,7 +81,7 @@ func (h *sharepointHandler) ConsumeRestoreCollections( switch dc.FullPath().Category() { case path.LibrariesCategory: - err = caches.Populate(ctx, lrh, rcc.ProtectedResource.ID()) + err = caches.Populate(ctx, h.apiClient.Users(), h.apiClient.Groups(), lrh, rcc.ProtectedResource.ID(), errs) if err != nil { return nil, nil, clues.Wrap(err, "initializing restore caches") } diff --git a/src/pkg/services/m365/api/groups.go b/src/pkg/services/m365/api/groups.go index 1c289a93f..7e2628ee3 100644 --- a/src/pkg/services/m365/api/groups.go +++ b/src/pkg/services/m365/api/groups.go @@ -12,6 +12,7 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/groups" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/str" "github.com/alcionai/corso/src/internal/common/tform" @@ -51,6 +52,25 @@ func (c Groups) GetAll( return getGroups(ctx, errs, service) } +// GetAllIDsAndNames retrieves all groups in the tenant and returns them in an idname.Cacher +func (c Groups) GetAllIDsAndNames(ctx context.Context, errs *fault.Bus) (idname.Cacher, error) { + all, err := c.GetAll(ctx, errs) + if err != nil { + return nil, clues.Wrap(err, "getting all users") + } + + idToName := make(map[string]string, len(all)) + + for _, g := range all { + id := strings.ToLower(ptr.Val(g.GetId())) + name := ptr.Val(g.GetDisplayName()) + + idToName[id] = name + } + + return idname.NewCache(idToName), nil +} + // GetAll retrieves all groups. func getGroups( ctx context.Context,