diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index 09af8b966..9e1d4bda2 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -13,6 +13,7 @@ import ( "github.com/alcionai/corso/src/cli/options" . "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/backup" @@ -194,6 +195,7 @@ func runBackups( r repository.Repository, serviceName, resourceOwnerType string, selectorSet []selectors.Selector, + ins common.IDNameSwapper, ) error { var ( bIDs []string @@ -203,21 +205,21 @@ func runBackups( for _, discSel := range selectorSet { var ( owner = discSel.DiscreteOwner - bctx = clues.Add(ctx, "resource_owner", owner) + ictx = clues.Add(ctx, "resource_owner", owner) ) - bo, err := r.NewBackup(bctx, discSel) + bo, err := r.NewBackup(ictx, discSel, ins) if err != nil { - errs = append(errs, clues.Wrap(err, owner).WithClues(bctx)) - Errf(bctx, "%v\n", err) + errs = append(errs, clues.Wrap(err, owner).WithClues(ictx)) + Errf(ictx, "%v\n", err) continue } - err = bo.Run(bctx) + err = bo.Run(ictx) if err != nil { - errs = append(errs, clues.Wrap(err, owner).WithClues(bctx)) - Errf(bctx, "%v\n", err) + errs = append(errs, clues.Wrap(err, owner).WithClues(ictx)) + Errf(ictx, "%v\n", err) continue } diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 07acce3cd..bf6fb410d 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -164,14 +164,14 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { // TODO: log/print recoverable errors errs := fault.New(false) - users, err := m365.UserPNs(ctx, *acct, errs) + ins, err := m365.UsersMap(ctx, *acct, errs) if err != nil { return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 user(s)")) } selectorSet := []selectors.Selector{} - for _, discSel := range sel.SplitByResourceOwner(users) { + for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) { selectorSet = append(selectorSet, discSel.Selector) } @@ -180,7 +180,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { r, "Exchange", "user", selectorSet, - ) + ins) } func exchangeBackupCreateSelectors(userIDs, cats []string) *selectors.ExchangeBackup { diff --git a/src/cli/backup/exchange_e2e_test.go b/src/cli/backup/exchange_e2e_test.go index cce596f10..a03f65e05 100644 --- a/src/cli/backup/exchange_e2e_test.go +++ b/src/cli/backup/exchange_e2e_test.go @@ -16,6 +16,7 @@ import ( "github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" @@ -300,7 +301,15 @@ func (suite *PreparedBackupExchangeE2ESuite) SetupSuite() { suite.backupOps = make(map[path.CategoryType]string) - users := []string{suite.m365UserID} + var ( + users = []string{suite.m365UserID} + idToName = map[string]string{suite.m365UserID: "todo-name-" + suite.m365UserID} + nameToID = map[string]string{"todo-name-" + suite.m365UserID: suite.m365UserID} + ins = common.IDsNames{ + IDToName: idToName, + NameToID: nameToID, + } + ) for _, set := range backupDataSets { var ( @@ -321,7 +330,7 @@ func (suite *PreparedBackupExchangeE2ESuite) SetupSuite() { sel.Include(scopes) - bop, err := suite.repo.NewBackup(ctx, sel.Selector) + bop, err := suite.repo.NewBackup(ctx, sel.Selector, ins) require.NoError(t, err, clues.ToCore(err)) err = bop.Run(ctx) @@ -546,7 +555,7 @@ func (suite *BackupDeleteExchangeE2ESuite) SetupSuite() { sel := selectors.NewExchangeBackup(users) sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch())) - suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector) + suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector, nil) require.NoError(t, err, clues.ToCore(err)) err = suite.backupOp.Run(ctx) diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index 96a9bff0a..31cffae7e 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -148,14 +148,14 @@ func createOneDriveCmd(cmd *cobra.Command, args []string) error { // TODO: log/print recoverable errors errs := fault.New(false) - users, err := m365.UserPNs(ctx, *acct, errs) + ins, err := m365.UsersMap(ctx, *acct, errs) if err != nil { return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 users")) } selectorSet := []selectors.Selector{} - for _, discSel := range sel.SplitByResourceOwner(users) { + for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) { selectorSet = append(selectorSet, discSel.Selector) } @@ -164,7 +164,7 @@ func createOneDriveCmd(cmd *cobra.Command, args []string) error { r, "OneDrive", "user", selectorSet, - ) + ins) } func validateOneDriveBackupCreateFlags(users []string) error { diff --git a/src/cli/backup/onedrive_e2e_test.go b/src/cli/backup/onedrive_e2e_test.go index fe0aed7eb..f73532ff4 100644 --- a/src/cli/backup/onedrive_e2e_test.go +++ b/src/cli/backup/onedrive_e2e_test.go @@ -16,6 +16,7 @@ import ( "github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" @@ -205,14 +206,22 @@ func (suite *BackupDeleteOneDriveE2ESuite) SetupSuite() { }) require.NoError(t, err, clues.ToCore(err)) - m365UserID := tester.M365UserID(t) - users := []string{m365UserID} + var ( + m365UserID = tester.M365UserID(t) + users = []string{m365UserID} + idToName = map[string]string{m365UserID: "todo-name-" + m365UserID} + nameToID = map[string]string{"todo-name-" + m365UserID: m365UserID} + ins = common.IDsNames{ + IDToName: idToName, + NameToID: nameToID, + } + ) // some tests require an existing backup sel := selectors.NewOneDriveBackup(users) sel.Include(sel.Folders(selectors.Any())) - suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector) + suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector, ins) require.NoError(t, err, clues.ToCore(err)) err = suite.backupOp.Run(ctx) diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index 76fd80f66..32062cb11 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -154,6 +154,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { // TODO: log/print recoverable errors errs := fault.New(false) + // TODO: discovery of sharepoint sites instead of early GC construction. gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), *acct, connector.Sites, errs) if err != nil { return Only(ctx, clues.Wrap(err, "Failed to connect to Microsoft APIs")) @@ -175,7 +176,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { r, "SharePoint", "site", selectorSet, - ) + nil) // TODO: prepopulate ids,names } func validateSharePointBackupCreateFlags(sites, weburls, cats []string) error { diff --git a/src/cli/backup/sharepoint_e2e_test.go b/src/cli/backup/sharepoint_e2e_test.go index dd5556e9e..f8b577024 100644 --- a/src/cli/backup/sharepoint_e2e_test.go +++ b/src/cli/backup/sharepoint_e2e_test.go @@ -16,6 +16,7 @@ import ( "github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" @@ -157,14 +158,22 @@ func (suite *BackupDeleteSharePointE2ESuite) SetupSuite() { suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) require.NoError(t, err, clues.ToCore(err)) - m365SiteID := tester.M365SiteID(t) - sites := []string{m365SiteID} + var ( + m365SiteID = tester.M365SiteID(t) + sites = []string{m365SiteID} + idToName = map[string]string{m365SiteID: "todo-name-" + m365SiteID} + nameToID = map[string]string{"todo-name-" + m365SiteID: m365SiteID} + ins = common.IDsNames{ + IDToName: idToName, + NameToID: nameToID, + } + ) // some tests require an existing backup sel := selectors.NewSharePointBackup(sites) sel.Include(testdata.SharePointBackupFolderScope(sel)) - suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector) + suite.backupOp, err = suite.repo.NewBackup(ctx, sel.Selector, ins) require.NoError(t, err, clues.ToCore(err)) err = suite.backupOp.Run(ctx) diff --git a/src/cli/restore/exchange_e2e_test.go b/src/cli/restore/exchange_e2e_test.go index 497f5ae70..03a783e62 100644 --- a/src/cli/restore/exchange_e2e_test.go +++ b/src/cli/restore/exchange_e2e_test.go @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/src/cli" "github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" @@ -73,7 +74,16 @@ func (suite *RestoreExchangeE2ESuite) SetupSuite() { suite.vpr, suite.cfgFP = tester.MakeTempTestConfigClone(t, force) suite.m365UserID = tester.M365UserID(t) - users := []string{suite.m365UserID} + + var ( + users = []string{suite.m365UserID} + idToName = map[string]string{suite.m365UserID: "todo-name-" + suite.m365UserID} + nameToID = map[string]string{"todo-name-" + suite.m365UserID: suite.m365UserID} + ins = common.IDsNames{ + IDToName: idToName, + NameToID: nameToID, + } + ) // init the repo first suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) @@ -100,7 +110,7 @@ func (suite *RestoreExchangeE2ESuite) SetupSuite() { sel.Include(scopes) - bop, err := suite.repo.NewBackup(ctx, sel.Selector) + bop, err := suite.repo.NewBackup(ctx, sel.Selector, ins) require.NoError(t, err, clues.ToCore(err)) err = bop.Run(ctx) diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go index 4a8ee1b52..e2dd3c1f7 100644 --- a/src/cmd/factory/impl/common.go +++ b/src/cmd/factory/impl/common.go @@ -126,12 +126,12 @@ func getGCAndVerifyUser(ctx context.Context, userID string) (*connector.GraphCon errs := fault.New(false) normUsers := map[string]struct{}{} - users, err := m365.UserPNs(ctx, acct, errs) + ins, err := m365.UsersMap(ctx, acct, errs) if err != nil { return nil, account.Account{}, clues.Wrap(err, "getting tenant users") } - for _, k := range users { + for _, k := range ins.IDs() { normUsers[strings.ToLower(k)] = struct{}{} } diff --git a/src/internal/common/idname.go b/src/internal/common/idname.go index fdc767226..efee8493f 100644 --- a/src/internal/common/idname.go +++ b/src/internal/common/idname.go @@ -1,5 +1,7 @@ package common +import "golang.org/x/exp/maps" + type IDNamer interface { // the canonical id of the thing, generated and usable // by whichever system has ownership of it. @@ -7,3 +9,39 @@ type IDNamer interface { // the human-readable name of the thing. Name() string } + +type IDNameSwapper interface { + IDOf(name string) (string, bool) + NameOf(id string) (string, bool) + IDs() []string + Names() []string +} + +var _ IDNameSwapper = &IDsNames{} + +type IDsNames struct { + IDToName map[string]string + NameToID map[string]string +} + +// IDOf returns the id associated with the given name. +func (in IDsNames) IDOf(name string) (string, bool) { + id, ok := in.NameToID[name] + return id, ok +} + +// NameOf returns the name associated with the given id. +func (in IDsNames) NameOf(id string) (string, bool) { + name, ok := in.IDToName[id] + return name, ok +} + +// IDs returns all known ids. +func (in IDsNames) IDs() []string { + return maps.Keys(in.IDToName) +} + +// Names returns all known names. +func (in IDsNames) Names() []string { + return maps.Keys(in.NameToID) +} diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index f3237ffec..36c8f34f6 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -17,6 +17,7 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/maps" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/graph" @@ -49,9 +50,16 @@ type GraphConnector struct { itemClient *http.Client // configured to handle large item downloads tenant string - Sites map[string]string // webURL -> siteID and siteID -> webURL credentials account.M365Config + // TODO: remove in favor of the maps below. + Sites map[string]string // webURL -> siteID and siteID -> webURL + + // lookup for resource owner ids to names, and names to ids. + // not guaranteed to be populated, only here as a post-population + // reference for processes that choose to populate the values. + IDNameLookup common.IDNameSwapper + // wg is used to track completion of GC tasks wg *sync.WaitGroup region *trace.Region @@ -108,6 +116,63 @@ func NewGraphConnector( return &gc, nil } +// PopulateOwnerIDAndNamesFrom takes the provided owner identifier and produces +// the owner's name and ID from that value. Returns an error if the owner is +// not recognized by the current tenant. +// +// The id-name swapper is optional. Some processes will look up all owners in +// the tenant before reaching this step. In that case, the data gets handed +// down for this func to consume instead of performing further queries. The +// maps get stored inside the gc instance for later re-use. +// +// TODO: If the maps are nil or empty, this func will perform a lookup on the given +// owner, and populate each map with that owner's id and name for downstream +// guarantees about that data being present. Optional performance enhancement +// idea: downstream from here, we should _only_ need the given user's id and name, +// and could store minimal map copies with that info instead of the whole tenant. +func (gc *GraphConnector) PopulateOwnerIDAndNamesFrom( + owner string, // input value, can be either id or name + ins common.IDNameSwapper, +) (string, string, error) { + // move this to GC method + id, name, err := getOwnerIDAndNameFrom(owner, ins) + if err != nil { + return "", "", errors.Wrap(err, "resolving resource owner details") + } + + gc.IDNameLookup = ins + + if ins == nil || (len(ins.IDs()) == 0 && len(ins.Names()) == 0) { + gc.IDNameLookup = common.IDsNames{ + IDToName: map[string]string{id: name}, + NameToID: map[string]string{name: id}, + } + } + + return id, name, nil +} + +func getOwnerIDAndNameFrom( + owner string, + ins common.IDNameSwapper, +) (string, string, error) { + if ins == nil { + return owner, owner, nil + } + + if n, ok := ins.NameOf(owner); ok { + return owner, n, nil + } else if i, ok := ins.IDOf(owner); ok { + return i, owner, nil + } + + // TODO: look-up user by owner, either id or name, + // and populate with maps as a result. Only + // return owner, owner as a very last resort. + + return owner, owner, nil +} + // createService constructor for graphService component func (gc *GraphConnector) createService() (*graph.Service, error) { adapter, err := graph.CreateAdapter( diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 5b93a2118..c704fc209 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/suite" "golang.org/x/exp/maps" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/connector/support" @@ -123,11 +124,11 @@ func (suite *GraphConnectorUnitSuite) TestUnionSiteIDsAndWebURLs() { } for _, test := range table { suite.Run(test.name, func() { - t := suite.T() - ctx, flush := tester.NewContext() defer flush() + t := suite.T() + result, err := gc.UnionSiteIDsAndWebURLs(ctx, test.ids, test.urls, fault.New(true)) assert.NoError(t, err, clues.ToCore(err)) assert.ElementsMatch(t, test.expect, result) @@ -135,6 +136,126 @@ func (suite *GraphConnectorUnitSuite) TestUnionSiteIDsAndWebURLs() { } } +func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() { + const ( + ownerID = "owner-id" + ownerName = "owner-name" + ) + + var ( + itn = map[string]string{ownerID: ownerName} + nti = map[string]string{ownerName: ownerID} + ) + + table := []struct { + name string + owner string + ins common.IDsNames + expectID string + expectName string + }{ + { + name: "nil ins", + owner: ownerID, + expectID: ownerID, + expectName: ownerID, + }, + { + name: "only id map with owner id", + owner: ownerID, + ins: common.IDsNames{ + IDToName: itn, + NameToID: nil, + }, + expectID: ownerID, + expectName: ownerName, + }, + { + name: "only name map with owner id", + owner: ownerID, + ins: common.IDsNames{ + IDToName: nil, + NameToID: nti, + }, + expectID: ownerID, + expectName: ownerID, + }, + { + name: "only id map with owner name", + owner: ownerName, + ins: common.IDsNames{ + IDToName: itn, + NameToID: nil, + }, + expectID: ownerName, + expectName: ownerName, + }, + { + name: "only name map with owner name", + owner: ownerName, + ins: common.IDsNames{ + IDToName: nil, + NameToID: nti, + }, + expectID: ownerID, + expectName: ownerName, + }, + { + name: "both maps with owner id", + owner: ownerID, + ins: common.IDsNames{ + IDToName: itn, + NameToID: nti, + }, + expectID: ownerID, + expectName: ownerName, + }, + { + name: "both maps with owner name", + owner: ownerName, + ins: common.IDsNames{ + IDToName: itn, + NameToID: nti, + }, + expectID: ownerID, + expectName: ownerName, + }, + { + name: "non-matching maps with owner id", + owner: ownerID, + ins: common.IDsNames{ + IDToName: map[string]string{"foo": "bar"}, + NameToID: map[string]string{"fnords": "smarf"}, + }, + expectID: ownerID, + expectName: ownerID, + }, + { + name: "non-matching with owner name", + owner: ownerName, + ins: common.IDsNames{ + IDToName: map[string]string{"foo": "bar"}, + NameToID: map[string]string{"fnords": "smarf"}, + }, + expectID: ownerName, + expectName: ownerName, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + var ( + t = suite.T() + gc = &GraphConnector{} + ) + + id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins) + require.NoError(t, err, clues.ToCore(err)) + assert.Equal(t, test.expectID, id) + assert.Equal(t, test.expectName, name) + }) + } +} + func (suite *GraphConnectorUnitSuite) TestGraphConnector_Wait() { ctx, flush := tester.NewContext() defer flush() diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index c8beef662..d9b52c9d3 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -110,6 +110,9 @@ func New( }, }, + ResourceOwnerID: ownerID, + ResourceOwnerName: ownerName, + Version: version.Backup, SnapshotID: snapshotID, StreamStoreID: streamStoreID, @@ -242,10 +245,20 @@ func (b Backup) Values() []string { status += (")") } + name := b.ResourceOwnerName + + if len(name) == 0 { + name = b.ResourceOwnerID + } + + if len(name) == 0 { + name = b.Selector.DiscreteOwner + } + return []string{ common.FormatTabularDisplayTime(b.StartedAt), string(b.ID), status, - b.Selector.DiscreteOwner, + name, } } diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 59beeb313..91bde1a17 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -24,7 +24,7 @@ func TestBackupUnitSuite(t *testing.T) { suite.Run(t, &BackupUnitSuite{Suite: tester.NewUnitSuite(t)}) } -func stubBackup(t time.Time) backup.Backup { +func stubBackup(t time.Time, ownerID, ownerName string) backup.Backup { sel := selectors.NewExchangeBackup([]string{"test"}) sel.Include(sel.AllData()) @@ -63,7 +63,7 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() { var ( t = suite.T() now = time.Now() - b = stubBackup(now) + b = stubBackup(now, "id", "name") expectHs = []string{ "Started At", "ID", @@ -190,7 +190,7 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() { func (suite *BackupUnitSuite) TestBackup_MinimumPrintable() { t := suite.T() now := time.Now() - b := stubBackup(now) + b := stubBackup(now, "id", "name") resultIface := b.MinimumPrintable() result, ok := resultIface.(backup.Printable) diff --git a/src/pkg/repository/loadtest/repository_load_test.go b/src/pkg/repository/loadtest/repository_load_test.go index 19d67fca8..5d7a597a2 100644 --- a/src/pkg/repository/loadtest/repository_load_test.go +++ b/src/pkg/repository/loadtest/repository_load_test.go @@ -120,7 +120,7 @@ func runLoadTest( ) { //revive:enable:context-as-argument t.Run(prefix+"_load_test_main", func(t *testing.T) { - b, err := r.NewBackup(ctx, bupSel) + b, err := r.NewBackup(ctx, bupSel, nil) require.NoError(t, err, clues.ToCore(err)) runBackupLoadTest(t, ctx, &b, service, usersUnderTest) @@ -447,8 +447,7 @@ func (suite *LoadExchangeSuite) TestExchange() { "all_users", "exchange", suite.usersUnderTest, sel, sel, // same selection for backup and restore - true, - ) + true) } // single user, lots of data @@ -500,8 +499,7 @@ func (suite *IndividualLoadExchangeSuite) TestExchange() { "single_user", "exchange", suite.usersUnderTest, sel, sel, // same selection for backup and restore - true, - ) + true) } // ------------------------------------------------------------------------------------------------ @@ -553,8 +551,7 @@ func (suite *LoadOneDriveSuite) TestOneDrive() { "all_users", "one_drive", suite.usersUnderTest, sel, sel, // same selection for backup and restore - false, - ) + false) } type IndividualLoadOneDriveSuite struct { @@ -601,8 +598,7 @@ func (suite *IndividualLoadOneDriveSuite) TestOneDrive() { "single_user", "one_drive", suite.usersUnderTest, sel, sel, // same selection for backup and restore - false, - ) + false) } // ------------------------------------------------------------------------------------------------ @@ -654,8 +650,7 @@ func (suite *LoadSharePointSuite) TestSharePoint() { "all_sites", "share_point", suite.sitesUnderTest, sel, sel, // same selection for backup and restore - false, - ) + false) } type IndividualLoadSharePointSuite struct { @@ -703,6 +698,5 @@ func (suite *IndividualLoadSharePointSuite) TestSharePoint() { "single_site", "share_point", suite.sitesUnderTest, sel, sel, // same selection for backup and restore - false, - ) + false) } diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index 1a9423677..63bde62a8 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common/crash" "github.com/alcionai/corso/src/internal/connector" "github.com/alcionai/corso/src/internal/connector/graph" @@ -58,6 +59,7 @@ type Repository interface { NewBackup( ctx context.Context, self selectors.Selector, + ins common.IDNameSwapper, ) (operations.BackupOperation, error) NewRestore( ctx context.Context, @@ -287,17 +289,25 @@ func (r *repository) Close(ctx context.Context) error { } // NewBackup generates a BackupOperation runner. +// ownerIDToName and ownerNameToID are optional populations, in case the caller has +// already generated those values. func (r repository) NewBackup( ctx context.Context, sel selectors.Selector, + ins common.IDNameSwapper, ) (operations.BackupOperation, error) { gc, err := connectToM365(ctx, sel, r.Account, fault.New(true)) if err != nil { return operations.BackupOperation{}, errors.Wrap(err, "connecting to m365") } + ownerID, ownerName, err := gc.PopulateOwnerIDAndNamesFrom(sel.DiscreteOwner, ins) + if err != nil { + return operations.BackupOperation{}, errors.Wrap(err, "resolving resource owner details") + } + // TODO: retrieve display name from gc - sel = sel.SetDiscreteOwnerIDName(sel.DiscreteOwner, "") + sel = sel.SetDiscreteOwnerIDName(ownerID, ownerName) return operations.NewBackupOperation( ctx, diff --git a/src/pkg/repository/repository_test.go b/src/pkg/repository/repository_test.go index cdea26d27..14871731d 100644 --- a/src/pkg/repository/repository_test.go +++ b/src/pkg/repository/repository_test.go @@ -198,7 +198,7 @@ func (suite *RepositoryIntegrationSuite) TestNewBackup() { r, err := repository.Initialize(ctx, acct, st, control.Options{}) require.NoError(t, err, clues.ToCore(err)) - bo, err := r.NewBackup(ctx, selectors.Selector{DiscreteOwner: "test"}) + bo, err := r.NewBackup(ctx, selectors.Selector{DiscreteOwner: "test"}, nil) require.NoError(t, err, clues.ToCore(err)) require.NotNil(t, bo) } diff --git a/src/pkg/services/m365/m365.go b/src/pkg/services/m365/m365.go index 4f620ce67..3815cdd46 100644 --- a/src/pkg/services/m365/m365.go +++ b/src/pkg/services/m365/m365.go @@ -6,6 +6,7 @@ import ( "github.com/alcionai/clues" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/connector" "github.com/alcionai/corso/src/internal/connector/discovery" @@ -56,34 +57,34 @@ func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, return ret, nil } -func UserIDs(ctx context.Context, acct account.Account, errs *fault.Bus) ([]string, error) { +// UsersMap retrieves all users in the tenant, and returns two maps: one id-to-principalName, +// and one principalName-to-id. +func UsersMap( + ctx context.Context, + acct account.Account, + errs *fault.Bus, +) (common.IDsNames, error) { users, err := Users(ctx, acct, errs) if err != nil { - return nil, err + return common.IDsNames{}, err } - ret := make([]string, 0, len(users)) + var ( + idToName = make(map[string]string, len(users)) + nameToID = make(map[string]string, len(users)) + ) + for _, u := range users { - ret = append(ret, u.ID) + idToName[u.ID] = u.PrincipalName + nameToID[u.PrincipalName] = u.ID } - return ret, nil -} - -// UserPNs retrieves all user principleNames in the tenant. Principle Names -// can be used analogous userIDs in graph API queries. -func UserPNs(ctx context.Context, acct account.Account, errs *fault.Bus) ([]string, error) { - users, err := Users(ctx, acct, errs) - if err != nil { - return nil, err + ins := common.IDsNames{ + IDToName: idToName, + NameToID: nameToID, } - ret := make([]string, 0, len(users)) - for _, u := range users { - ret = append(ret, u.PrincipalName) - } - - return ret, nil + return ins, nil } type Site struct {