Keepers 0451e933d5
handle no sharepoint license err (#3673)
adds handling for graph api responses
where the tenant does not have an active
sharepoint license.  For user's drives, this
will fall under the same "service not enabled"
behavior we use to determine if the drive can
get backed up.  For sites, this causes the
"get all sites" call to return a serviceNotEnabled error.

---

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

- [x]  Yes, it's included

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #3671

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2023-06-27 02:26:47 +00:00

341 lines
8.9 KiB
Go

package m365
import (
"context"
"github.com/alcionai/clues"
"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/m365/discovery"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
// ServiceAccess is true if a resource owner is capable of
// accessing or utilizing the specified service.
type ServiceAccess struct {
Exchange bool
// TODO: onedrive, sharepoint
}
// ---------------------------------------------------------------------------
// Users
// ---------------------------------------------------------------------------
// User is the minimal information required to identify and display a user.
type User struct {
PrincipalName string
ID string
Name string
Info api.UserInfo
}
// UserNoInfo is the minimal information required to identify and display a user.
// TODO: Remove this once `UsersCompatNoInfo` is removed
type UserNoInfo struct {
PrincipalName string
ID string
Name string
}
// UsersCompat returns a list of users in the specified M365 tenant.
// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault
// package.
func UsersCompat(ctx context.Context, acct account.Account) ([]*User, error) {
errs := fault.New(true)
us, err := Users(ctx, acct, errs)
if err != nil {
return nil, err
}
return us, errs.Failure()
}
// UsersCompatNoInfo returns a list of users in the specified M365 tenant.
// TODO: Remove this once `Info` is removed from the `User` struct and callers
// have switched over
func UsersCompatNoInfo(ctx context.Context, acct account.Account) ([]*UserNoInfo, error) {
errs := fault.New(true)
us, err := usersNoInfo(ctx, acct, errs)
if err != nil {
return nil, err
}
return us, errs.Failure()
}
// UserHasMailbox returns true if the user has an exchange mailbox enabled
// false otherwise, and a nil pointer and an error in case of error
func UserHasMailbox(ctx context.Context, acct account.Account, userID string) (bool, error) {
ac, err := makeAC(acct)
if err != nil {
return false, clues.Stack(err).WithClues(ctx)
}
_, err = ac.Users().GetMailInbox(ctx, userID)
if err != nil {
// we consider this a non-error case, since it
// answers the question the caller is asking.
if graph.IsErrExchangeMailFolderNotFound(err) {
return false, nil
}
if graph.IsErrUserNotFound(err) {
return false, clues.Stack(graph.ErrResourceOwnerNotFound, err)
}
if graph.IsErrExchangeMailFolderNotFound(err) {
return false, nil
}
return false, clues.Stack(err)
}
return true, nil
}
// UserHasDrives returns true if the user has any drives
// false otherwise, and a nil pointer and an error in case of error
func UserHasDrives(ctx context.Context, acct account.Account, userID string) (bool, error) {
ac, err := makeAC(acct)
if err != nil {
return false, clues.Stack(err).WithClues(ctx)
}
return checkUserHasDrives(ctx, ac.Users(), userID)
}
func checkUserHasDrives(ctx context.Context, dgdd discovery.GetDefaultDriver, userID string) (bool, error) {
_, err := dgdd.GetDefaultDrive(ctx, userID)
if err != nil {
// we consider this a non-error case, since it
// answers the question the caller is asking.
if clues.HasLabel(err, graph.LabelsMysiteNotFound) || clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
return false, nil
}
if graph.IsErrUserNotFound(err) {
return false, clues.Stack(graph.ErrResourceOwnerNotFound, err)
}
return false, clues.Stack(err)
}
return true, nil
}
// usersNoInfo returns a list of users in the specified M365 tenant - with no info
// TODO: Remove this once we remove `Info` from `Users` and instead rely on the `GetUserInfo` API
// to get user information
func usersNoInfo(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*UserNoInfo, error) {
ac, err := makeAC(acct)
if err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
us, err := discovery.Users(ctx, ac.Users(), errs)
if err != nil {
return nil, err
}
ret := make([]*UserNoInfo, 0, len(us))
for _, u := range us {
pu, err := parseUser(u)
if err != nil {
return nil, clues.Wrap(err, "formatting user data")
}
puNoInfo := &UserNoInfo{
PrincipalName: pu.PrincipalName,
ID: pu.ID,
Name: pu.Name,
}
ret = append(ret, puNoInfo)
}
return ret, nil
}
// Users returns a list of users in the specified M365 tenant
func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) {
ac, err := makeAC(acct)
if err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
us, err := discovery.Users(ctx, ac.Users(), errs)
if err != nil {
return nil, err
}
ret := make([]*User, 0, len(us))
for _, u := range us {
pu, err := parseUser(u)
if err != nil {
return nil, clues.Wrap(err, "formatting user data")
}
userInfo, err := discovery.GetUserInfo(ctx, acct, pu.ID, errs)
if err != nil {
return nil, clues.Wrap(err, "getting user details")
}
pu.Info = *userInfo
ret = append(ret, pu)
}
return ret, nil
}
// parseUser extracts information from `models.Userable` we care about
func parseUser(item models.Userable) (*User, error) {
if item.GetUserPrincipalName() == nil {
return nil, clues.New("user missing principal name").
With("user_id", ptr.Val(item.GetId()))
}
u := &User{
PrincipalName: ptr.Val(item.GetUserPrincipalName()),
ID: ptr.Val(item.GetId()),
Name: ptr.Val(item.GetDisplayName()),
}
return u, nil
}
// UserInfo returns the corso-specific set of user metadata.
func GetUserInfo(
ctx context.Context,
acct account.Account,
userID string,
) (*api.UserInfo, error) {
ac, err := makeAC(acct)
if err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
ui, err := discovery.UserInfo(ctx, ac.Users(), userID)
if err != nil {
return nil, err
}
return ui, nil
}
// ---------------------------------------------------------------------------
// Sites
// ---------------------------------------------------------------------------
// Site is the minimal information required to identify and display a SharePoint site.
type Site struct {
// WebURL is the url for the site, works as an alias for the user name.
WebURL string
// ID is of the format: <site collection hostname>.<site collection unique id>.<site unique id>
// for example: contoso.sharepoint.com,abcdeab3-0ccc-4ce1-80ae-b32912c9468d,xyzud296-9f7c-44e1-af81-3c06d0d43007
ID string
// DisplayName is the human-readable name of the site. Normally the plaintext name that the
// user provided when they created the site, though it can be changed across time.
// Ex: webUrl: https://host.com/sites/TestingSite, displayName: "Testing Site"
DisplayName string
}
// Sites returns a list of Sites in a specified M365 tenant
func Sites(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*Site, error) {
ac, err := makeAC(acct)
if err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return getAllSites(ctx, ac.Sites())
}
type getAllSiteser interface {
GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable, error)
}
func getAllSites(ctx context.Context, gas getAllSiteser) ([]*Site, error) {
sites, err := gas.GetAll(ctx, fault.New(true))
if err != nil {
if clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
return nil, clues.Stack(graph.ErrServiceNotEnabled, err)
}
return nil, clues.Wrap(err, "retrieving sites")
}
ret := make([]*Site, 0, len(sites))
for _, s := range sites {
ps, err := parseSite(s)
if err != nil {
return nil, clues.Wrap(err, "parsing siteable")
}
ret = append(ret, ps)
}
return ret, nil
}
// parseSite extracts the information from `models.Siteable` we care about
func parseSite(item models.Siteable) (*Site, error) {
s := &Site{
ID: ptr.Val(item.GetId()),
WebURL: ptr.Val(item.GetWebUrl()),
DisplayName: ptr.Val(item.GetDisplayName()),
}
return s, nil
}
// SitesMap retrieves all sites in the tenant, and returns two maps: one id-to-webURL,
// and one webURL-to-id.
func SitesMap(
ctx context.Context,
acct account.Account,
errs *fault.Bus,
) (idname.Cacher, error) {
sites, err := Sites(ctx, acct, errs)
if err != nil {
return idname.NewCache(nil), err
}
itn := make(map[string]string, len(sites))
for _, s := range sites {
itn[s.ID] = s.WebURL
}
return idname.NewCache(itn), nil
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
func makeAC(acct account.Account) (api.Client, error) {
creds, err := acct.M365Config()
if err != nil {
return api.Client{}, clues.Wrap(err, "getting m365 account creds")
}
cli, err := api.NewClient(creds)
if err != nil {
return api.Client{}, clues.Wrap(err, "constructing api client")
}
return cli, nil
}