Add GetUserInfo to services/m365 (#2991)
Adds an interface for GetUserInfo, which gets corso-specific metadata for a single user. Also does some refactoring around discovery for better interface consistency, both as variables and as access layers. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
b953eb1bd5
commit
3cc29649ed
@ -2,6 +2,7 @@ package backup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -169,6 +170,8 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 users"))
|
return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 users"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n-----\nINS %+v\n-----\n", ins)
|
||||||
|
|
||||||
selectorSet := []selectors.Selector{}
|
selectorSet := []selectors.Selector{}
|
||||||
|
|
||||||
for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) {
|
for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) {
|
||||||
|
|||||||
@ -293,7 +293,7 @@ func (suite *PreparedBackupExchangeE2ESuite) SetupSuite() {
|
|||||||
|
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
suite.m365UserID = tester.M365UserID(t)
|
suite.m365UserID = strings.ToLower(tester.M365UserID(t))
|
||||||
|
|
||||||
// init the repo first
|
// init the repo first
|
||||||
suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{})
|
suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{})
|
||||||
|
|||||||
@ -207,7 +207,7 @@ func (suite *BackupDeleteOneDriveE2ESuite) SetupSuite() {
|
|||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
m365UserID = tester.M365UserID(t)
|
m365UserID = strings.ToLower(tester.M365UserID(t))
|
||||||
users = []string{m365UserID}
|
users = []string{m365UserID}
|
||||||
idToName = map[string]string{m365UserID: m365UserID}
|
idToName = map[string]string{m365UserID: m365UserID}
|
||||||
nameToID = map[string]string{m365UserID: m365UserID}
|
nameToID = map[string]string{m365UserID: m365UserID}
|
||||||
|
|||||||
@ -159,7 +159,7 @@ func (suite *BackupDeleteSharePointE2ESuite) SetupSuite() {
|
|||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
m365SiteID = tester.M365SiteID(t)
|
m365SiteID = strings.ToLower(tester.M365SiteID(t))
|
||||||
sites = []string{m365SiteID}
|
sites = []string{m365SiteID}
|
||||||
idToName = map[string]string{m365SiteID: m365SiteID}
|
idToName = map[string]string{m365SiteID: m365SiteID}
|
||||||
nameToID = map[string]string{m365SiteID: m365SiteID}
|
nameToID = map[string]string{m365SiteID: m365SiteID}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package restore_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -73,7 +74,7 @@ func (suite *RestoreExchangeE2ESuite) SetupSuite() {
|
|||||||
}
|
}
|
||||||
suite.vpr, suite.cfgFP = tester.MakeTempTestConfigClone(t, force)
|
suite.vpr, suite.cfgFP = tester.MakeTempTestConfigClone(t, force)
|
||||||
|
|
||||||
suite.m365UserID = tester.M365UserID(t)
|
suite.m365UserID = strings.ToLower(tester.M365UserID(t))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
users = []string{suite.m365UserID}
|
users = []string{suite.m365UserID}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import "golang.org/x/exp/maps"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
type IDNamer interface {
|
type IDNamer interface {
|
||||||
// the canonical id of the thing, generated and usable
|
// the canonical id of the thing, generated and usable
|
||||||
@ -26,13 +30,13 @@ type IDsNames struct {
|
|||||||
|
|
||||||
// IDOf returns the id associated with the given name.
|
// IDOf returns the id associated with the given name.
|
||||||
func (in IDsNames) IDOf(name string) (string, bool) {
|
func (in IDsNames) IDOf(name string) (string, bool) {
|
||||||
id, ok := in.NameToID[name]
|
id, ok := in.NameToID[strings.ToLower(name)]
|
||||||
return id, ok
|
return id, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameOf returns the name associated with the given id.
|
// NameOf returns the name associated with the given id.
|
||||||
func (in IDsNames) NameOf(id string) (string, bool) {
|
func (in IDsNames) NameOf(id string) (string, bool) {
|
||||||
name, ok := in.IDToName[id]
|
name, ok := in.IDToName[strings.ToLower(id)]
|
||||||
return name, ok
|
return name, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common"
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/internal/connector/discovery"
|
"github.com/alcionai/corso/src/internal/connector/discovery"
|
||||||
"github.com/alcionai/corso/src/internal/connector/discovery/api"
|
|
||||||
"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"
|
||||||
@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
@ -171,7 +170,7 @@ func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {
|
|||||||
|
|
||||||
func checkServiceEnabled(
|
func checkServiceEnabled(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
au api.Users,
|
gi discovery.GetInfoer,
|
||||||
service path.ServiceType,
|
service path.ServiceType,
|
||||||
resource string,
|
resource string,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
@ -180,14 +179,13 @@ func checkServiceEnabled(
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, info, err := discovery.User(ctx, au, resource)
|
info, err := gi.GetInfo(ctx, resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := info.DiscoveredServices[service]; !ok {
|
if !info.ServiceEnabled(service) {
|
||||||
logger.Ctx(ctx).Error("service not enabled")
|
return false, clues.Wrap(graph.ErrServiceNotEnabled, "checking service access")
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|||||||
@ -45,6 +45,18 @@ func newUserInfo() *UserInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceEnabled returns true if the UserInfo has an entry for the
|
||||||
|
// service. If no entry exists, the service is assumed to not be enabled.
|
||||||
|
func (ui *UserInfo) ServiceEnabled(service path.ServiceType) bool {
|
||||||
|
if ui == nil || len(ui.DiscoveredServices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := ui.DiscoveredServices[service]
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// methods
|
// methods
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -160,7 +172,6 @@ func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
|
|||||||
|
|
||||||
// TODO: OneDrive
|
// TODO: OneDrive
|
||||||
_, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil)
|
_, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !graph.IsErrExchangeMailFolderNotFound(err) {
|
if !graph.IsErrExchangeMailFolderNotFound(err) {
|
||||||
return nil, graph.Wrap(ctx, err, "getting user's mail folder")
|
return nil, graph.Wrap(ctx, err, "getting user's mail folder")
|
||||||
|
|||||||
@ -20,13 +20,17 @@ type getter interface {
|
|||||||
GetByID(context.Context, string) (models.Userable, error)
|
GetByID(context.Context, string) (models.Userable, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type getInfoer interface {
|
type GetInfoer interface {
|
||||||
GetInfo(context.Context, string) (*api.UserInfo, error)
|
GetInfo(context.Context, string) (*api.UserInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type getWithInfoer interface {
|
type getWithInfoer interface {
|
||||||
getter
|
getter
|
||||||
getInfoer
|
GetInfoer
|
||||||
|
}
|
||||||
|
|
||||||
|
type getAller interface {
|
||||||
|
GetAll(ctx context.Context, errs *fault.Bus) ([]models.Userable, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -48,25 +52,28 @@ func apiClient(ctx context.Context, acct account.Account) (api.Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// api
|
// users
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Users fetches all users in the tenant.
|
// Users fetches all users in the tenant.
|
||||||
func Users(
|
func Users(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
acct account.Account,
|
ga getAller,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]models.Userable, error) {
|
) ([]models.Userable, error) {
|
||||||
client, err := apiClient(ctx, acct)
|
users, err := ga.GetAll(ctx, errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, clues.Wrap(err, "getting all users")
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Users().GetAll(ctx, errs)
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// User fetches a single user's data.
|
func User(
|
||||||
func User(ctx context.Context, gwi getWithInfoer, userID string) (models.Userable, *api.UserInfo, error) {
|
ctx context.Context,
|
||||||
|
gwi getWithInfoer,
|
||||||
|
userID string,
|
||||||
|
) (models.Userable, *api.UserInfo, error) {
|
||||||
u, err := gwi.GetByID(ctx, userID)
|
u, err := gwi.GetByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if graph.IsErrUserNotFound(err) {
|
if graph.IsErrUserNotFound(err) {
|
||||||
@ -84,6 +91,25 @@ func User(ctx context.Context, gwi getWithInfoer, userID string) (models.Userabl
|
|||||||
return u, ui, nil
|
return u, ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserInfo produces extensible user info: metadata that is relevant
|
||||||
|
// or identified in Corso, but not in m365.
|
||||||
|
func UserInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
gi GetInfoer,
|
||||||
|
userID string,
|
||||||
|
) (*api.UserInfo, error) {
|
||||||
|
ui, err := gi.GetInfo(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "getting user info")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// sites
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Sites fetches all sharepoint sites in the tenant
|
// Sites fetches all sharepoint sites in the tenant
|
||||||
func Sites(
|
func Sites(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@ -4,15 +4,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/connector/discovery"
|
"github.com/alcionai/corso/src/internal/connector/discovery"
|
||||||
|
"github.com/alcionai/corso/src/internal/connector/discovery/api"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/credentials"
|
"github.com/alcionai/corso/src/pkg/credentials"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscoveryIntegrationSuite struct {
|
type DiscoveryIntegrationSuite struct {
|
||||||
@ -37,7 +40,13 @@ func (suite *DiscoveryIntegrationSuite) TestUsers() {
|
|||||||
errs = fault.New(true)
|
errs = fault.New(true)
|
||||||
)
|
)
|
||||||
|
|
||||||
users, err := discovery.Users(ctx, acct, errs)
|
creds, err := acct.M365Config()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := api.NewClient(creds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
users, err := discovery.Users(ctx, cli.Users(), errs)
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
ferrs := errs.Errors()
|
ferrs := errs.Errors()
|
||||||
@ -47,9 +56,6 @@ func (suite *DiscoveryIntegrationSuite) TestUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() {
|
func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() {
|
||||||
ctx, flush := tester.NewContext()
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
acct func(t *testing.T) account.Account
|
acct func(t *testing.T) account.Account
|
||||||
@ -72,25 +78,23 @@ func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() {
|
|||||||
return a
|
return a
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Empty Credentials",
|
|
||||||
acct: func(t *testing.T) account.Account {
|
|
||||||
// intentionally swallowing the error here
|
|
||||||
a, _ := account.NewAccount(account.ProviderM365)
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
var (
|
ctx, flush := tester.NewContext()
|
||||||
t = suite.T()
|
defer flush()
|
||||||
a = test.acct(t)
|
|
||||||
errs = fault.New(true)
|
|
||||||
)
|
|
||||||
|
|
||||||
users, err := discovery.Users(ctx, a, errs)
|
t := suite.T()
|
||||||
|
acct := test.acct(t)
|
||||||
|
|
||||||
|
creds, err := acct.M365Config()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := api.NewClient(creds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
users, err := discovery.Users(ctx, cli.Users(), fault.New(true))
|
||||||
assert.Empty(t, users, "returned some users")
|
assert.Empty(t, users, "returned some users")
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
})
|
})
|
||||||
@ -166,3 +170,55 @@ func (suite *DiscoveryIntegrationSuite) TestSites_InvalidCredentials() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *DiscoveryIntegrationSuite) TestUserInfo() {
|
||||||
|
t := suite.T()
|
||||||
|
acct := tester.NewM365Account(t)
|
||||||
|
userID := tester.M365UserID(t)
|
||||||
|
|
||||||
|
creds, err := acct.M365Config()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := api.NewClient(creds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
uapi := cli.Users()
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
user string
|
||||||
|
expect *api.UserInfo
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard test user",
|
||||||
|
user: userID,
|
||||||
|
expect: &api.UserInfo{
|
||||||
|
DiscoveredServices: map[path.ServiceType]struct{}{
|
||||||
|
path.ExchangeService: {},
|
||||||
|
path.OneDriveService: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user does not exist",
|
||||||
|
user: uuid.NewString(),
|
||||||
|
expect: &api.UserInfo{
|
||||||
|
DiscoveredServices: map[path.ServiceType]struct{}{
|
||||||
|
path.OneDriveService: {}, // currently statically populated
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
result, err := discovery.UserInfo(ctx, uapi, test.user)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
assert.Equal(t, test.expect, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -61,6 +61,10 @@ var (
|
|||||||
// https://learn.microsoft.com/en-us/graph/errors#code-property
|
// https://learn.microsoft.com/en-us/graph/errors#code-property
|
||||||
ErrInvalidDelta = clues.New("invalid delta token")
|
ErrInvalidDelta = clues.New("invalid delta token")
|
||||||
|
|
||||||
|
// ErrServiceNotEnabled identifies that a resource owner does not have
|
||||||
|
// access to a given service.
|
||||||
|
ErrServiceNotEnabled = clues.New("service is not enabled for that resource owner")
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@ -152,7 +152,7 @@ func getOwnerIDAndNameFrom(
|
|||||||
// and populate with maps as a result. Only
|
// and populate with maps as a result. Only
|
||||||
// return owner, owner as a very last resort.
|
// return owner, owner as a very last resort.
|
||||||
|
|
||||||
return owner, owner, nil
|
return "", "", clues.New("not found within tenant")
|
||||||
}
|
}
|
||||||
|
|
||||||
// createService constructor for graphService component
|
// createService constructor for graphService component
|
||||||
|
|||||||
@ -56,12 +56,14 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
ins common.IDsNames
|
ins common.IDsNames
|
||||||
expectID string
|
expectID string
|
||||||
expectName string
|
expectName string
|
||||||
|
expectErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil ins",
|
name: "nil ins",
|
||||||
owner: ownerID,
|
owner: ownerID,
|
||||||
expectID: ownerID,
|
expectID: "",
|
||||||
expectName: ownerID,
|
expectName: "",
|
||||||
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only id map with owner id",
|
name: "only id map with owner id",
|
||||||
@ -72,6 +74,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: ownerID,
|
||||||
expectName: ownerName,
|
expectName: ownerName,
|
||||||
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only name map with owner id",
|
name: "only name map with owner id",
|
||||||
@ -80,8 +83,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
IDToName: nil,
|
IDToName: nil,
|
||||||
NameToID: nti,
|
NameToID: nti,
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: "",
|
||||||
expectName: ownerID,
|
expectName: "",
|
||||||
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only id map with owner name",
|
name: "only id map with owner name",
|
||||||
@ -90,8 +94,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
IDToName: itn,
|
IDToName: itn,
|
||||||
NameToID: nil,
|
NameToID: nil,
|
||||||
},
|
},
|
||||||
expectID: ownerName,
|
expectID: "",
|
||||||
expectName: ownerName,
|
expectName: "",
|
||||||
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only name map with owner name",
|
name: "only name map with owner name",
|
||||||
@ -102,6 +107,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: ownerID,
|
||||||
expectName: ownerName,
|
expectName: ownerName,
|
||||||
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both maps with owner id",
|
name: "both maps with owner id",
|
||||||
@ -112,6 +118,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: ownerID,
|
||||||
expectName: ownerName,
|
expectName: ownerName,
|
||||||
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both maps with owner name",
|
name: "both maps with owner name",
|
||||||
@ -122,6 +129,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: ownerID,
|
||||||
expectName: ownerName,
|
expectName: ownerName,
|
||||||
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-matching maps with owner id",
|
name: "non-matching maps with owner id",
|
||||||
@ -130,8 +138,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
IDToName: map[string]string{"foo": "bar"},
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
NameToID: map[string]string{"fnords": "smarf"},
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
expectID: "",
|
||||||
expectName: ownerID,
|
expectName: "",
|
||||||
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-matching with owner name",
|
name: "non-matching with owner name",
|
||||||
@ -140,8 +149,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
IDToName: map[string]string{"foo": "bar"},
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
NameToID: map[string]string{"fnords": "smarf"},
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
},
|
},
|
||||||
expectID: ownerName,
|
expectID: "",
|
||||||
expectName: ownerName,
|
expectName: "",
|
||||||
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
@ -152,7 +162,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
assert.Equal(t, test.expectID, id)
|
assert.Equal(t, test.expectID, id)
|
||||||
assert.Equal(t, test.expectName, name)
|
assert.Equal(t, test.expectName, name)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,16 +10,34 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common"
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/connector/discovery"
|
"github.com/alcionai/corso/src/internal/connector/discovery"
|
||||||
|
"github.com/alcionai/corso/src/internal/connector/discovery/api"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 {
|
type User struct {
|
||||||
PrincipalName string
|
PrincipalName string
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
ServicesEnabled ServiceAccess
|
||||||
|
}
|
||||||
|
|
||||||
// UsersCompat returns a list of users in the specified M365 tenant.
|
// UsersCompat returns a list of users in the specified M365 tenant.
|
||||||
// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault
|
// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault
|
||||||
// package.
|
// package.
|
||||||
@ -35,9 +53,13 @@ func UsersCompat(ctx context.Context, acct account.Account) ([]*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Users returns a list of users in the specified M365 tenant
|
// Users returns a list of users in the specified M365 tenant
|
||||||
// TODO: Implement paging support
|
|
||||||
func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) {
|
func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) {
|
||||||
users, err := discovery.Users(ctx, acct, errs)
|
uapi, err := makeUserAPI(acct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "getting users").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := discovery.Users(ctx, uapi, errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -47,7 +69,7 @@ func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User,
|
|||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
pu, err := parseUser(u)
|
pu, err := parseUser(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "parsing userable")
|
return nil, clues.Wrap(err, "formatting user data")
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, pu)
|
ret = append(ret, pu)
|
||||||
@ -103,8 +125,38 @@ func UsersMap(
|
|||||||
return ins, nil
|
return ins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserInfo returns the corso-specific set of user metadata.
|
||||||
|
func GetUserInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
acct account.Account,
|
||||||
|
userID string,
|
||||||
|
) (*UserInfo, error) {
|
||||||
|
uapi, err := makeUserAPI(acct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "getting user info").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui, err := discovery.UserInfo(ctx, uapi, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := UserInfo{
|
||||||
|
ServicesEnabled: ServiceAccess{
|
||||||
|
Exchange: ui.ServiceEnabled(path.ExchangeService),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sites
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Site is the minimal information required to identify and display a SharePoint site.
|
||||||
type Site struct {
|
type Site struct {
|
||||||
// WebURL that displays the item in the browser
|
// WebURL is the url for the site, works as an alias for the user name.
|
||||||
WebURL string
|
WebURL string
|
||||||
|
|
||||||
// ID is of the format: <site collection hostname>.<site collection unique id>.<site unique id>
|
// ID is of the format: <site collection hostname>.<site collection unique id>.<site unique id>
|
||||||
@ -173,3 +225,21 @@ func SitesMap(
|
|||||||
|
|
||||||
return ins, nil
|
return ins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func makeUserAPI(acct account.Account) (api.Users, error) {
|
||||||
|
creds, err := acct.M365Config()
|
||||||
|
if err != nil {
|
||||||
|
return api.Users{}, clues.Wrap(err, "getting m365 account creds")
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := api.NewClient(creds)
|
||||||
|
if err != nil {
|
||||||
|
return api.Users{}, clues.Wrap(err, "constructing api client")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.Users(), nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
package m365
|
package m365_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365"
|
||||||
)
|
)
|
||||||
|
|
||||||
type M365IntegrationSuite struct {
|
type M365IntegrationSuite struct {
|
||||||
@ -33,7 +35,7 @@ func (suite *M365IntegrationSuite) TestUsers() {
|
|||||||
acct = tester.NewM365Account(suite.T())
|
acct = tester.NewM365Account(suite.T())
|
||||||
)
|
)
|
||||||
|
|
||||||
users, err := Users(ctx, acct, fault.New(true))
|
users, err := m365.Users(ctx, acct, fault.New(true))
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
assert.NotEmpty(t, users)
|
assert.NotEmpty(t, users)
|
||||||
|
|
||||||
@ -48,6 +50,30 @@ func (suite *M365IntegrationSuite) TestUsers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *M365IntegrationSuite) TestGetUserInfo() {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
acct = tester.NewM365Account(t)
|
||||||
|
uid = tester.M365UserID(t)
|
||||||
|
)
|
||||||
|
|
||||||
|
info, err := m365.GetUserInfo(ctx, acct, uid)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, info)
|
||||||
|
require.NotEmpty(t, info)
|
||||||
|
|
||||||
|
expect := &m365.UserInfo{
|
||||||
|
ServicesEnabled: m365.ServiceAccess{
|
||||||
|
Exchange: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expect, info)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *M365IntegrationSuite) TestSites() {
|
func (suite *M365IntegrationSuite) TestSites() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
@ -57,7 +83,7 @@ func (suite *M365IntegrationSuite) TestSites() {
|
|||||||
acct = tester.NewM365Account(suite.T())
|
acct = tester.NewM365Account(suite.T())
|
||||||
)
|
)
|
||||||
|
|
||||||
sites, err := Sites(ctx, acct, fault.New(true))
|
sites, err := m365.Sites(ctx, acct, fault.New(true))
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
assert.NotEmpty(t, sites)
|
assert.NotEmpty(t, sites)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user