From 5ff890b760423eba46c4725873e5e2f6db438919 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 19 Oct 2022 09:41:22 -0400 Subject: [PATCH] GC: Backup: OneDrive: Call Backup on User without OneDrive Access FIX (#1197) ## Description Corso should be able to have `Backup` called on a user that does not have OneDrive or does not have OneDrive files without producing an error. This feature branch creates OneDriveIntegration testing to be able to fetch OneDrive data for a particular user. Logic changed within `drive.go` ## Type of change - [x] :sunflower: Feature - [x] :bug: Bugfix ## Issue(s) **closes** #966 ## Test Plan - [x] :zap: Unit test --- .../connector/onedrive/collections_test.go | 15 --- src/internal/connector/onedrive/drive.go | 110 +++++++++++++++++- src/internal/connector/onedrive/drive_test.go | 92 +++++++-------- .../connector/onedrive/service_test.go | 85 ++++++++++++++ 4 files changed, 238 insertions(+), 64 deletions(-) create mode 100644 src/internal/connector/onedrive/service_test.go diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 299a63576..01512d89e 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -3,7 +3,6 @@ package onedrive import ( "testing" - msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -207,17 +206,3 @@ func driveItem(name string, path string, isFile, isFolder, isPackage bool) model return item } - -type MockGraphService struct{} - -func (ms *MockGraphService) Client() *msgraphsdk.GraphServiceClient { - return nil -} - -func (ms *MockGraphService) Adapter() *msgraphsdk.GraphRequestAdapter { - return nil -} - -func (ms *MockGraphService) ErrPolicy() bool { - return false -} diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 147f532d3..e0db895d0 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" "github.com/microsoftgraph/msgraph-sdk-go/drives/item/items" "github.com/microsoftgraph/msgraph-sdk-go/drives/item/items/item" "github.com/microsoftgraph/msgraph-sdk-go/drives/item/root/delta" @@ -17,7 +18,43 @@ import ( "github.com/alcionai/corso/src/pkg/logger" ) -var errFolderNotFound = errors.New("folder not found") +var ( + errFolderNotFound = errors.New("folder not found") + + // nolint:lll + // OneDrive associated SKUs located at: + // https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference + skuIDs = []string{ + // Microsoft 365 Apps for Business 0365 + "cdd28e44-67e3-425e-be4c-737fab2899d3", + // Microsoft 365 Apps for Business SMB_Business + "b214fe43-f5a3-4703-beeb-fa97188220fc", + // Microsoft 365 Apps for enterprise + "c2273bd0-dff7-4215-9ef5-2c7bcfb06425", + // Microsoft 365 Apps for Faculty + "12b8c807-2e20-48fc-b453-542b6ee9d171", + // Microsoft 365 Apps for Students + "c32f9321-a627-406d-a114-1f9c81aaafac", + // OneDrive for Business (Plan 1) + "e6778190-713e-4e4f-9119-8b8238de25df", + // OneDrive for Business (Plan 2) + "ed01faf2-1d88-4947-ae91-45ca18703a96", + // Visio Plan 1 + "ca7f3140-d88c-455b-9a1c-7f0679e31a76", + // Visio Plan 2 + "38b434d2-a15e-4cde-9a98-e737c75623e1", + // Visio Online Plan 1 + "4b244418-9658-4451-a2b8-b5e2b364e9bd", + // Visio Online Plan 2 + "c5928f49-12ba-48f7-ada3-0d743a3601d5", + // Visio Plan 2 for GCC + "4ae99959-6b0f-43b0-b1ce-68146001bdba", + // ONEDRIVEENTERPRISE + "afcafa6a-d966-4462-918c-ec0b4e0fe642", + // Microsoft 365 E5 Developer + "c42b9cae-ea4f-4ab7-9717-81576235ccac", + } +) const ( // nextLinkKey is used to find the next link in a paged @@ -26,12 +63,30 @@ const ( itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children" itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s" itemNotFoundErrorCode = "itemNotFound" + userDoesNotHaveDrive = "BadRequest Unable to retrieve user's mysite URL" ) // Enumerates the drives for the specified user func drives(ctx context.Context, service graph.Service, user string) ([]models.Driveable, error) { + var hasDrive bool + + hasDrive, err := hasDriveLicense(ctx, service, user) + if err != nil { + return nil, errors.Wrap(err, user) + } + + if !hasDrive { + logger.Ctx(ctx).Debugf("User %s does not have a license for OneDrive", user) + return make([]models.Driveable, 0), nil // no license + } + r, err := service.Client().UsersById(user).Drives().Get(ctx, nil) if err != nil { + if strings.Contains(support.ConnectorStackErrorTrace(err), userDoesNotHaveDrive) { + logger.Ctx(ctx).Debugf("User %s does not have a drive", user) + return make([]models.Driveable, 0), nil // no license + } + return nil, errors.Wrapf(err, "failed to retrieve user drives. user: %s, details: %s", user, support.ConnectorStackErrorTrace(err)) } @@ -239,3 +294,56 @@ func DeleteItem( return nil } + +// hasDriveLicense utility function that queries M365 server +// to investigate the user's includes access to OneDrive. +func hasDriveLicense( + ctx context.Context, + service graph.Service, + user string, +) (bool, error) { + var hasDrive bool + + resp, err := service.Client().UsersById(user).LicenseDetails().Get(ctx, nil) + if err != nil { + return false, + errors.Wrap(err, "failure obtaining license details for user") + } + + iter, err := msgraphgocore.NewPageIterator( + resp, service.Adapter(), + models.CreateLicenseDetailsCollectionResponseFromDiscriminatorValue, + ) + if err != nil { + return false, err + } + + cb := func(pageItem any) bool { + entry, ok := pageItem.(models.LicenseDetailsable) + if !ok { + err = errors.New("casting item to models.MailFolderable") + return false + } + + sku := entry.GetSkuId() + if sku == nil { + return true + } + + for _, license := range skuIDs { + if *sku == license { + hasDrive = true + return false + } + } + + return true + } + + if err := iter.Iterate(ctx, cb); err != nil { + return false, + errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + } + + return hasDrive, nil +} diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 08e6e0459..8302e9873 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -5,61 +5,15 @@ import ( "strings" "testing" - msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/common" - "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/tester" - "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/selectors" ) -// TODO(ashmrtn): Merge with similar structs in graph and exchange packages. -type testService struct { - adapter msgraphsdk.GraphRequestAdapter - client msgraphsdk.GraphServiceClient - failFast bool - credentials account.M365Config -} - -func (ts *testService) Client() *msgraphsdk.GraphServiceClient { - return &ts.client -} - -func (ts *testService) Adapter() *msgraphsdk.GraphRequestAdapter { - return &ts.adapter -} - -func (ts *testService) ErrPolicy() bool { - return ts.failFast -} - -// TODO(ashmrtn): Merge with similar functions in connector and exchange -// packages. -func loadService(t *testing.T) *testService { - a := tester.NewM365Account(t) - m365, err := a.M365Config() - require.NoError(t, err) - - adapter, err := graph.CreateAdapter( - m365.AzureTenantID, - m365.AzureClientID, - m365.AzureClientSecret, - ) - require.NoError(t, err) - - service := &testService{ - adapter: *adapter, - client: *msgraphsdk.NewGraphServiceClient(adapter), - failFast: false, - credentials: m365, - } - - return service -} - type OneDriveSuite struct { suite.Suite userID string @@ -86,7 +40,7 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() { folderIDs := []string{} folderName1 := "Corso_Folder_Test_" + common.FormatNow(common.SimpleTimeTesting) folderElements := []string{folderName1} - gs := loadService(t) + gs := loadTestService(t) drives, err := drives(ctx, gs, suite.userID) require.NoError(t, err) @@ -144,3 +98,45 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() { }) } } + +func (suite *OneDriveSuite) TestOneDriveNewCollections() { + ctx, flush := tester.NewContext() + defer flush() + + creds, err := tester.NewM365Account(suite.T()).M365Config() + require.NoError(suite.T(), err) + + tests := []struct { + name, user string + }{ + { + name: "Test User w/ Drive", + user: suite.userID, + }, + { + name: "Test User w/out Drive", + user: "testevents@8qzvrj.onmicrosoft.com", + }, + } + + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + service := loadTestService(t) + scope := selectors. + NewOneDriveBackup(). + Users([]string{test.user})[0] + odcs, err := NewCollections( + creds.AzureTenantID, + test.user, + scope, + service, + service.updateStatus, + ).Get(ctx) + assert.NoError(t, err) + + for _, entry := range odcs { + assert.NotEmpty(t, entry.FullPath()) + } + }) + } +} diff --git a/src/internal/connector/onedrive/service_test.go b/src/internal/connector/onedrive/service_test.go new file mode 100644 index 000000000..da67ecfc1 --- /dev/null +++ b/src/internal/connector/onedrive/service_test.go @@ -0,0 +1,85 @@ +package onedrive + +import ( + "testing" + + msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + "github.com/stretchr/testify/require" + + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/account" +) + +type MockGraphService struct{} + +func (ms *MockGraphService) Client() *msgraphsdk.GraphServiceClient { + return nil +} + +func (ms *MockGraphService) Adapter() *msgraphsdk.GraphRequestAdapter { + return nil +} + +func (ms *MockGraphService) ErrPolicy() bool { + return false +} + +// TODO(ashmrtn): Merge with similar structs in graph and exchange packages. +type oneDriveService struct { + client msgraphsdk.GraphServiceClient + adapter msgraphsdk.GraphRequestAdapter + credentials account.M365Config + status support.ConnectorOperationStatus +} + +func (ods *oneDriveService) Client() *msgraphsdk.GraphServiceClient { + return &ods.client +} + +func (ods *oneDriveService) Adapter() *msgraphsdk.GraphRequestAdapter { + return &ods.adapter +} + +func (ods *oneDriveService) ErrPolicy() bool { + return false +} + +func NewOneDriveService(credentials account.M365Config) (*oneDriveService, error) { + adapter, err := graph.CreateAdapter( + credentials.AzureTenantID, + credentials.AzureClientID, + credentials.AzureClientSecret, + ) + if err != nil { + return nil, err + } + + service := oneDriveService{ + adapter: *adapter, + client: *msgraphsdk.NewGraphServiceClient(adapter), + credentials: credentials, + } + + return &service, nil +} + +func (ods *oneDriveService) updateStatus(status *support.ConnectorOperationStatus) { + if status == nil { + return + } + + ods.status = support.MergeStatus(ods.status, *status) +} + +func loadTestService(t *testing.T) *oneDriveService { + a := tester.NewM365Account(t) + m365, err := a.M365Config() + require.NoError(t, err) + + service, err := NewOneDriveService(m365) + require.NoError(t, err) + + return service +}