Check whether the user has an exchange mailbox (#2156)

## Description

This commit adds logic in discovery and backup to check whether the specified user has
an exchange mailbox that is available/enabled.

If so - the backup is short-circuited to succeed but with "no data"

Going forward - we should be able to move the logic in the OneDrive connector that checks
for a valid drive and license in here.

## Does this PR need a docs update or release note?

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No 

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #2145 

## Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Vaibhav Kamra 2023-01-17 22:59:29 -08:00 committed by GitHub
parent 63b77e2bf5
commit 3a37584938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 5 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] (alpha) ## [Unreleased] (alpha)
### Fixed
- Check if the user specified for an exchange backup operation has a mailbox.
## [v0.1.0] (alpha) - 2023-01-13 ## [v0.1.0] (alpha) - 2023-01-13

View File

@ -7,7 +7,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/connector/sharepoint"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
@ -15,6 +17,7 @@ import (
D "github.com/alcionai/corso/src/internal/diagnostics" D "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
@ -41,6 +44,15 @@ func (gc *GraphConnector) DataCollections(
return nil, err return nil, err
} }
serviceEnabled, err := checkServiceEnabled(ctx, gc.Service, path.ServiceType(sels.Service), sels.DiscreteOwner)
if err != nil {
return nil, err
}
if !serviceEnabled {
return []data.Collection{}, nil
}
switch sels.Service { switch sels.Service {
case selectors.ServiceExchange: case selectors.ServiceExchange:
colls, err := exchange.DataCollections( colls, err := exchange.DataCollections(
@ -124,6 +136,29 @@ func verifyBackupInputs(sels selectors.Selector, userPNs, siteIDs []string) erro
return nil return nil
} }
func checkServiceEnabled(
ctx context.Context,
gs graph.Servicer,
service path.ServiceType,
resource string,
) (bool, error) {
if service == path.SharePointService {
// No "enabled" check required for sharepoint
return true, nil
}
_, info, err := discovery.User(ctx, gs, resource)
if err != nil {
return false, err
}
if _, ok := info.DiscoveredServices[service]; !ok {
return false, nil
}
return true, nil
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// OneDrive // OneDrive
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/path"
) )
const ( const (
@ -64,6 +65,49 @@ func Users(ctx context.Context, gs graph.Servicer, tenantID string) ([]models.Us
return users, iterErrs return users, iterErrs
} }
type UserInfo struct {
DiscoveredServices map[path.ServiceType]struct{}
}
func User(ctx context.Context, gs graph.Servicer, userID string) (models.Userable, *UserInfo, error) {
user, err := gs.Client().UsersById(userID).Get(ctx, nil)
if err != nil {
return nil, nil, errors.Wrapf(
err,
"retrieving resource for tenant: %s",
support.ConnectorStackErrorTrace(err),
)
}
// Assume all services are enabled
userInfo := &UserInfo{
DiscoveredServices: map[path.ServiceType]struct{}{
path.ExchangeService: {},
path.OneDriveService: {},
},
}
// Discover which services the user has enabled
// Exchange: Query `MailFolders`
_, err = gs.Client().UsersById(userID).MailFolders().Get(ctx, nil)
if err != nil {
if !graph.IsErrExchangeMailFolderNotFound(err) {
return nil, nil, errors.Wrapf(
err,
"retrieving mail folders for tenant: %s",
support.ConnectorStackErrorTrace(err),
)
}
delete(userInfo.DiscoveredServices, path.ExchangeService)
}
// TODO: OneDrive
return user, userInfo, nil
}
// parseUser extracts information from `models.Userable` we care about // parseUser extracts information from `models.Userable` we care about
func parseUser(item interface{}) (models.Userable, error) { func parseUser(item interface{}) (models.Userable, error) {
m, ok := item.(models.Userable) m, ok := item.(models.Userable)

View File

@ -15,11 +15,13 @@ import (
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const ( const (
errCodeItemNotFound = "ErrorItemNotFound" errCodeItemNotFound = "ErrorItemNotFound"
errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound"
errCodeResyncRequired = "ResyncRequired" errCodeResyncRequired = "ResyncRequired"
errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound"
errCodeSyncStateNotFound = "SyncStateNotFound" errCodeSyncStateNotFound = "SyncStateNotFound"
errCodeResourceNotFound = "ResourceNotFound"
errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI"
) )
// The folder or item was deleted between the time we identified // The folder or item was deleted between the time we identified
@ -69,6 +71,10 @@ func asInvalidDelta(err error) bool {
return errors.As(err, &e) return errors.As(err, &e)
} }
func IsErrExchangeMailFolderNotFound(err error) bool {
return hasErrorCode(err, errCodeResourceNotFound, errCodeMailboxNotEnabledForRESTAPI)
}
// Timeout errors are identified for tracking the need to retry calls. // Timeout errors are identified for tracking the need to retry calls.
// Other delay errors, like throttling, are already handled by the // Other delay errors, like throttling, are already handled by the
// graph client's built-in retries. // graph client's built-in retries.