look up users and sites by id or name (#2973)
Adds a lookup step to graph connector to find an owner's id and name given some identifier. The identifier, for either sites or users, can be a well formed id or name. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Issue(s) * #2825 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
47cf403cca
commit
896c05f623
@ -253,12 +253,12 @@ func runBackups(
|
|||||||
// genericDeleteCommand is a helper function that all services can use
|
// genericDeleteCommand is a helper function that all services can use
|
||||||
// for the removal of an entry from the repository
|
// for the removal of an entry from the repository
|
||||||
func genericDeleteCommand(cmd *cobra.Command, bID, designation string, args []string) error {
|
func genericDeleteCommand(cmd *cobra.Command, bID, designation string, args []string) error {
|
||||||
ctx := clues.Add(cmd.Context(), "delete_backup_id", bID)
|
|
||||||
|
|
||||||
if utils.HasNoFlagsAndShownHelp(cmd) {
|
if utils.HasNoFlagsAndShownHelp(cmd) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := clues.Add(cmd.Context(), "delete_backup_id", bID)
|
||||||
|
|
||||||
r, _, err := getAccountAndConnect(ctx)
|
r, _, err := getAccountAndConnect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
|
|||||||
@ -235,7 +235,7 @@ func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_UserNotInTenant() {
|
|||||||
assert.Contains(
|
assert.Contains(
|
||||||
t,
|
t,
|
||||||
err.Error(),
|
err.Error(),
|
||||||
"not found within tenant", "error missing user not found")
|
"not found in tenant", "error missing user not found")
|
||||||
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
||||||
|
|
||||||
t.Logf("backup error message: %s", err.Error())
|
t.Logf("backup error message: %s", err.Error())
|
||||||
|
|||||||
@ -140,7 +140,7 @@ func (suite *NoBackupOneDriveE2ESuite) TestOneDriveBackupCmd_UserNotInTenant() {
|
|||||||
assert.Contains(
|
assert.Contains(
|
||||||
t,
|
t,
|
||||||
err.Error(),
|
err.Error(),
|
||||||
"not found within tenant", "error missing user not found")
|
"not found in tenant", "error missing user not found")
|
||||||
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
assert.NotContains(t, err.Error(), "runtime error", "panic happened")
|
||||||
|
|
||||||
t.Logf("backup error message: %s", err.Error())
|
t.Logf("backup error message: %s", err.Error())
|
||||||
|
|||||||
@ -53,7 +53,7 @@ func (gc *GraphConnector) ProduceBackupCollections(
|
|||||||
|
|
||||||
serviceEnabled, err := checkServiceEnabled(
|
serviceEnabled, err := checkServiceEnabled(
|
||||||
ctx,
|
ctx,
|
||||||
gc.Owners.Users(),
|
gc.Discovery.Users(),
|
||||||
path.ServiceType(sels.Service),
|
path.ServiceType(sels.Service),
|
||||||
sels.DiscreteOwner)
|
sels.DiscreteOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -162,7 +162,7 @@ func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return clues.New("resource owner not found within tenant").With("missing_resource_owner", sels.DiscreteOwner)
|
return clues.Stack(graph.ErrResourceOwnerNotFound).With("missing_resource_owner", sels.DiscreteOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -25,27 +25,21 @@ import (
|
|||||||
// DataCollection tests
|
// DataCollection tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type ConnectorDataCollectionIntegrationSuite struct {
|
type DataCollectionIntgSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
connector *GraphConnector
|
user string
|
||||||
user string
|
site string
|
||||||
site string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConnectorDataCollectionIntegrationSuite(t *testing.T) {
|
func TestDataCollectionIntgSuite(t *testing.T) {
|
||||||
suite.Run(t, &ConnectorDataCollectionIntegrationSuite{
|
suite.Run(t, &DataCollectionIntgSuite{
|
||||||
Suite: tester.NewIntegrationSuite(
|
Suite: tester.NewIntegrationSuite(
|
||||||
t,
|
t,
|
||||||
[][]string{tester.M365AcctCredEnvs},
|
[][]string{tester.M365AcctCredEnvs}),
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConnectorDataCollectionIntegrationSuite) SetupSuite() {
|
func (suite *DataCollectionIntgSuite) SetupSuite() {
|
||||||
ctx, flush := tester.NewContext()
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
suite.connector = loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), AllResources)
|
|
||||||
suite.user = tester.M365UserID(suite.T())
|
suite.user = tester.M365UserID(suite.T())
|
||||||
suite.site = tester.M365SiteID(suite.T())
|
suite.site = tester.M365SiteID(suite.T())
|
||||||
|
|
||||||
@ -58,7 +52,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) SetupSuite() {
|
|||||||
// - mail
|
// - mail
|
||||||
// - contacts
|
// - contacts
|
||||||
// - events
|
// - events
|
||||||
func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection() {
|
func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -138,7 +132,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestInvalidUserForDataCollections ensures verification process for users
|
// TestInvalidUserForDataCollections ensures verification process for users
|
||||||
func (suite *ConnectorDataCollectionIntegrationSuite) TestDataCollections_invalidResourceOwner() {
|
func (suite *DataCollectionIntgSuite) TestDataCollections_invalidResourceOwner() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -223,7 +217,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestDataCollections_invali
|
|||||||
// TestSharePointDataCollection verifies interface between operation and
|
// TestSharePointDataCollection verifies interface between operation and
|
||||||
// GraphConnector remains stable to receive a non-zero amount of Collections
|
// GraphConnector remains stable to receive a non-zero amount of Collections
|
||||||
// for the SharePoint Package.
|
// for the SharePoint Package.
|
||||||
func (suite *ConnectorDataCollectionIntegrationSuite) TestSharePointDataCollection() {
|
func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -299,14 +293,14 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestSharePointDataCollecti
|
|||||||
// CreateSharePointCollection tests
|
// CreateSharePointCollection tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type ConnectorCreateSharePointCollectionIntegrationSuite struct {
|
type SPCollectionIntgSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
connector *GraphConnector
|
connector *GraphConnector
|
||||||
user string
|
user string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConnectorCreateSharePointCollectionIntegrationSuite(t *testing.T) {
|
func TestSPCollectionIntgSuite(t *testing.T) {
|
||||||
suite.Run(t, &ConnectorCreateSharePointCollectionIntegrationSuite{
|
suite.Run(t, &SPCollectionIntgSuite{
|
||||||
Suite: tester.NewIntegrationSuite(
|
Suite: tester.NewIntegrationSuite(
|
||||||
t,
|
t,
|
||||||
[][]string{tester.M365AcctCredEnvs},
|
[][]string{tester.M365AcctCredEnvs},
|
||||||
@ -314,7 +308,7 @@ func TestConnectorCreateSharePointCollectionIntegrationSuite(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) SetupSuite() {
|
func (suite *SPCollectionIntgSuite) SetupSuite() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -324,7 +318,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) SetupSuite() {
|
|||||||
tester.LogTimeOfTest(suite.T())
|
tester.LogTimeOfTest(suite.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection_Libraries() {
|
func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -335,7 +329,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar
|
|||||||
siteIDs = []string{siteID}
|
siteIDs = []string{siteID}
|
||||||
)
|
)
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(siteID, nil)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(ctx, siteID, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
sel := selectors.NewSharePointBackup(siteIDs)
|
sel := selectors.NewSharePointBackup(siteIDs)
|
||||||
@ -368,7 +362,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar
|
|||||||
cols[1].FullPath().Service().String())
|
cols[1].FullPath().Service().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection_Lists() {
|
func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
|
||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
@ -379,7 +373,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar
|
|||||||
siteIDs = []string{siteID}
|
siteIDs = []string{siteID}
|
||||||
)
|
)
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(siteID, nil)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(ctx, siteID, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
sel := selectors.NewSharePointBackup(siteIDs)
|
sel := selectors.NewSharePointBackup(siteIDs)
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -12,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||||
|
"github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -85,15 +88,74 @@ func (c Sites) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable,
|
|||||||
return us, el.Failure()
|
return us, el.Failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Sites) GetByID(ctx context.Context, id string) (models.Siteable, error) {
|
const uuidRE = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||||
resp, err := c.stable.Client().SitesById(id).Get(ctx, nil)
|
|
||||||
|
// matches a site ID, with or without a doman name. Ex, either one of:
|
||||||
|
// 10rqc2.sharepoint.com,deadbeef-0000-0000-0000-000000000000,beefdead-0000-0000-0000-000000000000
|
||||||
|
// deadbeef-0000-0000-0000-000000000000,beefdead-0000-0000-0000-000000000000
|
||||||
|
var siteIDRE = regexp.MustCompile(`(.+,)?` + uuidRE + "," + uuidRE)
|
||||||
|
|
||||||
|
const webURLGetTemplate = "https://graph.microsoft.com/v1.0/sites/%s:/%s"
|
||||||
|
|
||||||
|
// GetByID looks up the site matching the given identifier. The identifier can be either a
|
||||||
|
// canonical site id or a webURL. Assumes the webURL is complete and well formed;
|
||||||
|
// eg: https://10rqc2.sharepoint.com/sites/Example
|
||||||
|
func (c Sites) GetByID(ctx context.Context, identifier string) (models.Siteable, error) {
|
||||||
|
var (
|
||||||
|
resp models.Siteable
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = clues.Add(ctx, "given_site_id", identifier)
|
||||||
|
|
||||||
|
if siteIDRE.MatchString(identifier) {
|
||||||
|
resp, err = c.stable.Client().SitesById(identifier).Get(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Wrap(ctx, err, "getting site by id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the id is not a standard sharepoint ID, assume it's a url.
|
||||||
|
// if it has a leading slash, assume it's only a path. If it doesn't,
|
||||||
|
// ensure it has a prefix https://
|
||||||
|
if !strings.HasPrefix(identifier, "/") {
|
||||||
|
identifier = strings.TrimPrefix(identifier, "https://")
|
||||||
|
identifier = "https://" + identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "getting site")
|
return nil, clues.Wrap(err, "site is not parseable as a url")
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't construct a path with double leading slashes
|
||||||
|
path := strings.TrimPrefix(u.Path, "/")
|
||||||
|
rawURL := fmt.Sprintf(webURLGetTemplate, u.Host, path)
|
||||||
|
|
||||||
|
resp, err = sites.
|
||||||
|
NewItemSitesSiteItemRequestBuilder(rawURL, c.stable.Adapter()).
|
||||||
|
Get(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Wrap(ctx, err, "getting site by weburl")
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIDAndName looks up the site matching the given ID, and returns
|
||||||
|
// its canonical ID and the webURL as the name. Accepts an ID or a
|
||||||
|
// WebURL as an ID.
|
||||||
|
func (c Sites) GetIDAndName(ctx context.Context, siteID string) (string, string, error) {
|
||||||
|
s, err := c.GetByID(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptr.Val(s.GetId()), ptr.Val(s.GetWebUrl()), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// helpers
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -116,23 +178,23 @@ func validateSite(item any) (models.Siteable, error) {
|
|||||||
return nil, clues.New("missing ID")
|
return nil, clues.New("missing ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
url := ptr.Val(m.GetWebUrl())
|
wURL := ptr.Val(m.GetWebUrl())
|
||||||
if len(url) == 0 {
|
if len(wURL) == 0 {
|
||||||
return nil, clues.New("missing webURL").With("site_id", id) // TODO: pii
|
return nil, clues.New("missing webURL").With("site_id", id) // TODO: pii
|
||||||
}
|
}
|
||||||
|
|
||||||
// personal (ie: oneDrive) sites have to be filtered out server-side.
|
// personal (ie: oneDrive) sites have to be filtered out server-side.
|
||||||
if strings.Contains(url, personalSitePath) {
|
if strings.Contains(wURL, personalSitePath) {
|
||||||
return nil, clues.Stack(errKnownSkippableCase).
|
return nil, clues.Stack(errKnownSkippableCase).
|
||||||
With("site_id", id, "site_url", url) // TODO: pii
|
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
||||||
}
|
}
|
||||||
|
|
||||||
name := ptr.Val(m.GetDisplayName())
|
name := ptr.Val(m.GetDisplayName())
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
// the built-in site at "https://{tenant-domain}/search" never has a name.
|
// the built-in site at "https://{tenant-domain}/search" never has a name.
|
||||||
if strings.HasSuffix(url, "/search") {
|
if strings.HasSuffix(wURL, "/search") {
|
||||||
return nil, clues.Stack(errKnownSkippableCase).
|
return nil, clues.Stack(errKnownSkippableCase).
|
||||||
With("site_id", id, "site_url", url) // TODO: pii
|
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, clues.New("missing site display name").With("site_id", id)
|
return nil, clues.New("missing site display name").With("site_id", id)
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -153,3 +155,48 @@ func (suite *SitesIntgSuite) TestGetAll() {
|
|||||||
assert.NotContains(t, ptr.Val(site.GetWebUrl()), personalSitePath, "must not return onedrive sites")
|
assert.NotContains(t, ptr.Val(site.GetWebUrl()), personalSitePath, "must not return onedrive sites")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *SitesIntgSuite) TestSites_GetByID() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
siteID = tester.M365SiteID(t)
|
||||||
|
host = strings.Split(siteID, ",")[0]
|
||||||
|
shortID = strings.TrimPrefix(siteID, host+",")
|
||||||
|
siteURL = tester.M365SiteURL(t)
|
||||||
|
acct = tester.NewM365Account(t)
|
||||||
|
)
|
||||||
|
|
||||||
|
creds, err := acct.M365Config()
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
client, err := NewClient(creds)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
sitesAPI := client.Sites()
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
id string
|
||||||
|
expectErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{"3 part id", siteID, assert.NoError},
|
||||||
|
{"2 part id", shortID, assert.NoError},
|
||||||
|
{"malformed id", uuid.NewString(), assert.Error},
|
||||||
|
{"random id", uuid.NewString() + "," + uuid.NewString(), assert.Error},
|
||||||
|
{"url", siteURL, assert.NoError},
|
||||||
|
{"host only", host, assert.NoError},
|
||||||
|
{"malformed url", "barunihlda", assert.Error},
|
||||||
|
{"non-matching url", "https://test/sites/testing", assert.Error},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
_, err := sitesAPI.GetByID(ctx, test.id)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/users"
|
"github.com/microsoftgraph/msgraph-sdk-go/users"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -147,13 +148,15 @@ func (c Users) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Userable,
|
|||||||
return us, el.Failure()
|
return us, el.Failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Users) GetByID(ctx context.Context, userID string) (models.Userable, error) {
|
// GetByID looks up the user matching the given identifier. The identifier can be either a
|
||||||
|
// canonical user id or a princpalName.
|
||||||
|
func (c Users) GetByID(ctx context.Context, identifier string) (models.Userable, error) {
|
||||||
var (
|
var (
|
||||||
resp models.Userable
|
resp models.Userable
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
resp, err = c.stable.Client().UsersById(userID).Get(ctx, nil)
|
resp, err = c.stable.Client().UsersById(identifier).Get(ctx, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "getting user")
|
return nil, graph.Wrap(ctx, err, "getting user")
|
||||||
@ -162,6 +165,17 @@ func (c Users) GetByID(ctx context.Context, userID string) (models.Userable, err
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIDAndName looks up the user matching the given ID, and returns
|
||||||
|
// its canonical ID and the PrincipalName as the name.
|
||||||
|
func (c Users) GetIDAndName(ctx context.Context, userID string) (string, string, error) {
|
||||||
|
u, err := c.GetByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptr.Val(u.GetId()), ptr.Val(u.GetUserPrincipalName()), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
|
func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
|
||||||
// Assume all services are enabled
|
// Assume all services are enabled
|
||||||
// then filter down to only services the user has enabled
|
// then filter down to only services the user has enabled
|
||||||
|
|||||||
@ -77,7 +77,7 @@ func User(
|
|||||||
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) {
|
||||||
return nil, nil, clues.New("resource owner not found within tenant").With("user_id", userID)
|
return nil, nil, clues.Stack(graph.ErrResourceOwnerNotFound).With("user_id", userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, clues.Wrap(err, "getting user")
|
return nil, nil, clues.Wrap(err, "getting user")
|
||||||
|
|||||||
@ -70,6 +70,8 @@ var (
|
|||||||
// graph client's built-in retries.
|
// graph client's built-in retries.
|
||||||
// https://github.com/microsoftgraph/msgraph-sdk-go/issues/302
|
// https://github.com/microsoftgraph/msgraph-sdk-go/issues/302
|
||||||
ErrTimeout = clues.New("communication timeout")
|
ErrTimeout = clues.New("communication timeout")
|
||||||
|
|
||||||
|
ErrResourceOwnerNotFound = clues.New("resource owner not found in tenant")
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsErrDeletedInFlight(err error) bool {
|
func IsErrDeletedInFlight(err error) bool {
|
||||||
@ -191,7 +193,11 @@ func Wrap(ctx context.Context, e error, msg string) *clues.Err {
|
|||||||
return clues.Wrap(e, msg).WithClues(ctx)
|
return clues.Wrap(e, msg).WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, innerMsg := errData(odErr)
|
mainMsg, data, innerMsg := errData(odErr)
|
||||||
|
|
||||||
|
if len(mainMsg) > 0 {
|
||||||
|
e = clues.Stack(e, clues.New(mainMsg))
|
||||||
|
}
|
||||||
|
|
||||||
return setLabels(clues.Wrap(e, msg).WithClues(ctx).With(data...), innerMsg)
|
return setLabels(clues.Wrap(e, msg).WithClues(ctx).With(data...), innerMsg)
|
||||||
}
|
}
|
||||||
@ -208,7 +214,11 @@ func Stack(ctx context.Context, e error) *clues.Err {
|
|||||||
return clues.Stack(e).WithClues(ctx)
|
return clues.Stack(e).WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, innerMsg := errData(odErr)
|
mainMsg, data, innerMsg := errData(odErr)
|
||||||
|
|
||||||
|
if len(mainMsg) > 0 {
|
||||||
|
e = clues.Stack(e, clues.New(mainMsg))
|
||||||
|
}
|
||||||
|
|
||||||
return setLabels(clues.Stack(e).WithClues(ctx).With(data...), innerMsg)
|
return setLabels(clues.Stack(e).WithClues(ctx).With(data...), innerMsg)
|
||||||
}
|
}
|
||||||
@ -226,11 +236,12 @@ func setLabels(err *clues.Err, msg string) *clues.Err {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func errData(err odataerrors.ODataErrorable) ([]any, string) {
|
func errData(err odataerrors.ODataErrorable) (string, []any, string) {
|
||||||
data := make([]any, 0)
|
data := make([]any, 0)
|
||||||
|
|
||||||
// Get MainError
|
// Get MainError
|
||||||
mainErr := err.GetError()
|
mainErr := err.GetError()
|
||||||
|
mainMsg := ptr.Val(mainErr.GetMessage())
|
||||||
|
|
||||||
data = appendIf(data, "odataerror_code", mainErr.GetCode())
|
data = appendIf(data, "odataerror_code", mainErr.GetCode())
|
||||||
data = appendIf(data, "odataerror_message", mainErr.GetMessage())
|
data = appendIf(data, "odataerror_message", mainErr.GetMessage())
|
||||||
@ -251,7 +262,7 @@ func errData(err odataerrors.ODataErrorable) ([]any, string) {
|
|||||||
data = appendIf(data, "odataerror_inner_req_id", inner.GetRequestId())
|
data = appendIf(data, "odataerror_inner_req_id", inner.GetRequestId())
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, strings.ToLower(msgConcat)
|
return mainMsg, data, strings.ToLower(msgConcat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendIf(a []any, k string, v *string) []any {
|
func appendIf(a []any, k string, v *string) []any {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common"
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/internal/connector/discovery/api"
|
"github.com/alcionai/corso/src/internal/connector/discovery/api"
|
||||||
@ -36,12 +35,13 @@ var (
|
|||||||
// bookkeeping and interfacing with other component.
|
// bookkeeping and interfacing with other component.
|
||||||
type GraphConnector struct {
|
type GraphConnector struct {
|
||||||
Service graph.Servicer
|
Service graph.Servicer
|
||||||
Owners api.Client
|
Discovery api.Client
|
||||||
itemClient *http.Client // configured to handle large item downloads
|
itemClient *http.Client // configured to handle large item downloads
|
||||||
|
|
||||||
tenant string
|
tenant string
|
||||||
credentials account.M365Config
|
credentials account.M365Config
|
||||||
|
|
||||||
|
ownerLookup getOwnerIDAndNamer
|
||||||
// maps of resource owner ids to names, and names to ids.
|
// maps of resource owner ids to names, and names to ids.
|
||||||
// not guaranteed to be populated, only here as a post-population
|
// not guaranteed to be populated, only here as a post-population
|
||||||
// reference for processes that choose to populate the values.
|
// reference for processes that choose to populate the values.
|
||||||
@ -56,15 +56,6 @@ type GraphConnector struct {
|
|||||||
status support.ConnectorOperationStatus // contains the status of the last run status
|
status support.ConnectorOperationStatus // contains the status of the last run status
|
||||||
}
|
}
|
||||||
|
|
||||||
type resource int
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnknownResource resource = iota
|
|
||||||
AllResources
|
|
||||||
Users
|
|
||||||
Sites
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewGraphConnector(
|
func NewGraphConnector(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
itemClient *http.Client,
|
itemClient *http.Client,
|
||||||
@ -72,95 +63,51 @@ func NewGraphConnector(
|
|||||||
r resource,
|
r resource,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) (*GraphConnector, error) {
|
) (*GraphConnector, error) {
|
||||||
m365, err := acct.M365Config()
|
creds, err := acct.M365Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "retrieving m365 account configuration").WithClues(ctx)
|
return nil, clues.Wrap(err, "retrieving m365 account configuration").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
gc := GraphConnector{
|
service, err := createService(creds)
|
||||||
itemClient: itemClient,
|
|
||||||
tenant: m365.AzureTenantID,
|
|
||||||
wg: &sync.WaitGroup{},
|
|
||||||
credentials: m365,
|
|
||||||
IDNameLookup: common.IDsNames{},
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.Service, err = gc.createService()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "creating service connection").WithClues(ctx)
|
return nil, clues.Wrap(err, "creating service connection").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.Owners, err = api.NewClient(m365)
|
discovery, err := api.NewClient(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc, err := r.resourceClient(discovery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "creating resource client").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
gc := GraphConnector{
|
||||||
|
Discovery: discovery,
|
||||||
|
IDNameLookup: common.IDsNames{},
|
||||||
|
Service: service,
|
||||||
|
|
||||||
|
credentials: creds,
|
||||||
|
itemClient: itemClient,
|
||||||
|
ownerLookup: rc,
|
||||||
|
tenant: acct.ID(),
|
||||||
|
wg: &sync.WaitGroup{},
|
||||||
|
}
|
||||||
|
|
||||||
return &gc, nil
|
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
|
// Service Client
|
||||||
// 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 "", "", clues.New("not found within tenant")
|
|
||||||
}
|
|
||||||
|
|
||||||
// createService constructor for graphService component
|
// createService constructor for graphService component
|
||||||
func (gc *GraphConnector) createService() (*graph.Service, error) {
|
func createService(creds account.M365Config) (*graph.Service, error) {
|
||||||
adapter, err := graph.CreateAdapter(
|
adapter, err := graph.CreateAdapter(
|
||||||
gc.credentials.AzureTenantID,
|
creds.AzureTenantID,
|
||||||
gc.credentials.AzureClientID,
|
creds.AzureClientID,
|
||||||
gc.credentials.AzureClientSecret)
|
creds.AzureClientSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &graph.Service{}, err
|
return &graph.Service{}, err
|
||||||
}
|
}
|
||||||
@ -168,6 +115,10 @@ func (gc *GraphConnector) createService() (*graph.Service, error) {
|
|||||||
return graph.NewService(adapter), nil
|
return graph.NewService(adapter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Processing Status
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// AwaitStatus waits for all gc tasks to complete and then returns status
|
// AwaitStatus waits for all gc tasks to complete and then returns status
|
||||||
func (gc *GraphConnector) Wait() *data.CollectionStats {
|
func (gc *GraphConnector) Wait() *data.CollectionStats {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -223,3 +174,129 @@ func (gc *GraphConnector) incrementAwaitingMessages() {
|
|||||||
func (gc *GraphConnector) incrementMessagesBy(num int) {
|
func (gc *GraphConnector) incrementMessagesBy(num int) {
|
||||||
gc.wg.Add(num)
|
gc.wg.Add(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Resource Lookup Handling
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type resource int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnknownResource resource = iota
|
||||||
|
AllResources // unused
|
||||||
|
Users
|
||||||
|
Sites
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r resource) resourceClient(discovery api.Client) (*resourceClient, error) {
|
||||||
|
switch r {
|
||||||
|
case Users:
|
||||||
|
return &resourceClient{enum: r, getter: discovery.Users()}, nil
|
||||||
|
case Sites:
|
||||||
|
return &resourceClient{enum: r, getter: discovery.Sites()}, nil
|
||||||
|
default:
|
||||||
|
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceClient struct {
|
||||||
|
enum resource
|
||||||
|
getter getIDAndNamer
|
||||||
|
}
|
||||||
|
|
||||||
|
type getIDAndNamer interface {
|
||||||
|
GetIDAndName(ctx context.Context, owner string) (
|
||||||
|
ownerID string,
|
||||||
|
ownerName string,
|
||||||
|
err error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ getOwnerIDAndNamer = &resourceClient{}
|
||||||
|
|
||||||
|
type getOwnerIDAndNamer interface {
|
||||||
|
getOwnerIDAndNameFrom(
|
||||||
|
ctx context.Context,
|
||||||
|
discovery api.Client,
|
||||||
|
owner string,
|
||||||
|
ins common.IDNameSwapper,
|
||||||
|
) (
|
||||||
|
ownerID string,
|
||||||
|
ownerName string,
|
||||||
|
err error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOwnerIDAndNameFrom looks up the owner's canonical id and display name.
|
||||||
|
// If the owner is present in the idNameSwapper, then that interface's id and
|
||||||
|
// name values are returned. As a fallback, the resource calls the discovery
|
||||||
|
// api to fetch the user or site using the owner value. This fallback assumes
|
||||||
|
// that the owner is a well formed ID or display name of appropriate design
|
||||||
|
// (PrincipalName for users, WebURL for sites).
|
||||||
|
func (r resourceClient) getOwnerIDAndNameFrom(
|
||||||
|
ctx context.Context,
|
||||||
|
discovery api.Client,
|
||||||
|
owner string,
|
||||||
|
ins common.IDNameSwapper,
|
||||||
|
) (string, string, error) {
|
||||||
|
if ins != nil {
|
||||||
|
if n, ok := ins.NameOf(owner); ok {
|
||||||
|
return owner, n, nil
|
||||||
|
} else if i, ok := ins.IDOf(owner); ok {
|
||||||
|
return i, owner, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = clues.Add(ctx, "owner_identifier", owner)
|
||||||
|
|
||||||
|
var (
|
||||||
|
id, name string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// if r.enum == Sites {
|
||||||
|
// TODO: check all suffixes in nameToID
|
||||||
|
// }
|
||||||
|
|
||||||
|
id, name, err = r.getter.GetIDAndName(ctx, owner)
|
||||||
|
if err != nil {
|
||||||
|
if graph.IsErrUserNotFound(err) {
|
||||||
|
return "", "", clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(id) == 0 || len(name) == 0 {
|
||||||
|
return "", "", clues.Stack(graph.ErrResourceOwnerNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, name, 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
|
||||||
|
// data gets stored inside the gc instance for later re-use.
|
||||||
|
func (gc *GraphConnector) PopulateOwnerIDAndNamesFrom(
|
||||||
|
ctx context.Context,
|
||||||
|
owner string, // input value, can be either id or name
|
||||||
|
ins common.IDNameSwapper,
|
||||||
|
) (string, string, error) {
|
||||||
|
// move this to GC method
|
||||||
|
id, name, err := gc.ownerLookup.getOwnerIDAndNameFrom(ctx, gc.Discovery, owner, ins)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", clues.Wrap(err, "identifying resource owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.IDNameLookup = common.IDsNames{
|
||||||
|
IDToName: map[string]string{id: name},
|
||||||
|
NameToID: map[string]string{name: id},
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, name, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -452,11 +452,11 @@ func (suite *GraphConnectorSharePointIntegrationSuite) SetupSuite() {
|
|||||||
|
|
||||||
si.resourceOwner = tester.M365SiteID(suite.T())
|
si.resourceOwner = tester.M365SiteID(suite.T())
|
||||||
|
|
||||||
user, err := si.connector.Owners.Users().GetByID(ctx, si.user)
|
user, err := si.connector.Discovery.Users().GetByID(ctx, si.user)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
||||||
si.userID = ptr.Val(user.GetId())
|
si.userID = ptr.Val(user.GetId())
|
||||||
|
|
||||||
secondaryUser, err := si.connector.Owners.Users().GetByID(ctx, si.secondaryUser)
|
secondaryUser, err := si.connector.Discovery.Users().GetByID(ctx, si.secondaryUser)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
||||||
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
||||||
|
|
||||||
@ -499,11 +499,11 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) SetupSuite() {
|
|||||||
|
|
||||||
si.resourceOwner = si.user
|
si.resourceOwner = si.user
|
||||||
|
|
||||||
user, err := si.connector.Owners.Users().GetByID(ctx, si.user)
|
user, err := si.connector.Discovery.Users().GetByID(ctx, si.user)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
||||||
si.userID = ptr.Val(user.GetId())
|
si.userID = ptr.Val(user.GetId())
|
||||||
|
|
||||||
secondaryUser, err := si.connector.Owners.Users().GetByID(ctx, si.secondaryUser)
|
secondaryUser, err := si.connector.Discovery.Users().GetByID(ctx, si.secondaryUser)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
||||||
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
||||||
|
|
||||||
@ -558,11 +558,11 @@ func (suite *GraphConnectorOneDriveNightlySuite) SetupSuite() {
|
|||||||
|
|
||||||
si.resourceOwner = si.user
|
si.resourceOwner = si.user
|
||||||
|
|
||||||
user, err := si.connector.Owners.Users().GetByID(ctx, si.user)
|
user, err := si.connector.Discovery.Users().GetByID(ctx, si.user)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.user, clues.ToCore(err))
|
||||||
si.userID = ptr.Val(user.GetId())
|
si.userID = ptr.Val(user.GetId())
|
||||||
|
|
||||||
secondaryUser, err := si.connector.Owners.Users().GetByID(ctx, si.secondaryUser)
|
secondaryUser, err := si.connector.Discovery.Users().GetByID(ctx, si.secondaryUser)
|
||||||
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
require.NoError(suite.T(), err, "fetching user", si.secondaryUser, clues.ToCore(err))
|
||||||
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
|
||||||
|
|
||||||
|
|||||||
@ -39,132 +39,231 @@ func TestGraphConnectorUnitSuite(t *testing.T) {
|
|||||||
suite.Run(t, &GraphConnectorUnitSuite{Suite: tester.NewUnitSuite(t)})
|
suite.Run(t, &GraphConnectorUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ getIDAndNamer = &mockNameIDGetter{}
|
||||||
|
|
||||||
|
type mockNameIDGetter struct {
|
||||||
|
id, name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mnig mockNameIDGetter) GetIDAndName(
|
||||||
|
_ context.Context,
|
||||||
|
_ string,
|
||||||
|
) (string, string, error) {
|
||||||
|
return mnig.id, mnig.name, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
|
||||||
const (
|
const (
|
||||||
ownerID = "owner-id"
|
id = "owner-id"
|
||||||
ownerName = "owner-name"
|
name = "owner-name"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
itn = map[string]string{ownerID: ownerName}
|
itn = map[string]string{id: name}
|
||||||
nti = map[string]string{ownerName: ownerID}
|
nti = map[string]string{name: id}
|
||||||
|
lookup = &resourceClient{
|
||||||
|
enum: Users,
|
||||||
|
getter: &mockNameIDGetter{id: id, name: name},
|
||||||
|
}
|
||||||
|
noLookup = &resourceClient{enum: Users, getter: &mockNameIDGetter{}}
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
owner string
|
owner string
|
||||||
ins common.IDsNames
|
ins common.IDsNames
|
||||||
|
rc *resourceClient
|
||||||
expectID string
|
expectID string
|
||||||
expectName string
|
expectName string
|
||||||
expectErr require.ErrorAssertionFunc
|
expectErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil ins",
|
name: "nil ins",
|
||||||
owner: ownerID,
|
owner: id,
|
||||||
|
rc: lookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil ins no lookup",
|
||||||
|
owner: id,
|
||||||
|
rc: noLookup,
|
||||||
expectID: "",
|
expectID: "",
|
||||||
expectName: "",
|
expectName: "",
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only id map with owner id",
|
name: "only id map with owner id",
|
||||||
owner: ownerID,
|
owner: id,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: itn,
|
IDToName: itn,
|
||||||
NameToID: nil,
|
NameToID: nil,
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
rc: noLookup,
|
||||||
expectName: ownerName,
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only name map with owner id",
|
name: "only name map with owner id",
|
||||||
owner: ownerID,
|
owner: id,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: nil,
|
IDToName: nil,
|
||||||
NameToID: nti,
|
NameToID: nti,
|
||||||
},
|
},
|
||||||
|
rc: noLookup,
|
||||||
expectID: "",
|
expectID: "",
|
||||||
expectName: "",
|
expectName: "",
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "only name map with owner id and lookup",
|
||||||
|
owner: id,
|
||||||
|
ins: common.IDsNames{
|
||||||
|
IDToName: nil,
|
||||||
|
NameToID: nti,
|
||||||
|
},
|
||||||
|
rc: lookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "only id map with owner name",
|
name: "only id map with owner name",
|
||||||
owner: ownerName,
|
owner: name,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: itn,
|
IDToName: itn,
|
||||||
NameToID: nil,
|
NameToID: nil,
|
||||||
},
|
},
|
||||||
|
rc: lookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only name map with owner name",
|
||||||
|
owner: name,
|
||||||
|
ins: common.IDsNames{
|
||||||
|
IDToName: nil,
|
||||||
|
NameToID: nti,
|
||||||
|
},
|
||||||
|
rc: noLookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only id map with owner name",
|
||||||
|
owner: name,
|
||||||
|
ins: common.IDsNames{
|
||||||
|
IDToName: itn,
|
||||||
|
NameToID: nil,
|
||||||
|
},
|
||||||
|
rc: noLookup,
|
||||||
expectID: "",
|
expectID: "",
|
||||||
expectName: "",
|
expectName: "",
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only name map with owner name",
|
name: "only id map with owner name and lookup",
|
||||||
owner: ownerName,
|
owner: name,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: nil,
|
IDToName: itn,
|
||||||
NameToID: nti,
|
NameToID: nil,
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
rc: lookup,
|
||||||
expectName: ownerName,
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both maps with owner id",
|
name: "both maps with owner id",
|
||||||
owner: ownerID,
|
owner: id,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: itn,
|
IDToName: itn,
|
||||||
NameToID: nti,
|
NameToID: nti,
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
rc: noLookup,
|
||||||
expectName: ownerName,
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both maps with owner name",
|
name: "both maps with owner name",
|
||||||
owner: ownerName,
|
owner: name,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: itn,
|
IDToName: itn,
|
||||||
NameToID: nti,
|
NameToID: nti,
|
||||||
},
|
},
|
||||||
expectID: ownerID,
|
rc: noLookup,
|
||||||
expectName: ownerName,
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
expectErr: require.NoError,
|
expectErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-matching maps with owner id",
|
name: "non-matching maps with owner id",
|
||||||
owner: ownerID,
|
owner: id,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: map[string]string{"foo": "bar"},
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
NameToID: map[string]string{"fnords": "smarf"},
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
},
|
},
|
||||||
|
rc: noLookup,
|
||||||
expectID: "",
|
expectID: "",
|
||||||
expectName: "",
|
expectName: "",
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-matching with owner name",
|
name: "non-matching with owner name",
|
||||||
owner: ownerName,
|
owner: name,
|
||||||
ins: common.IDsNames{
|
ins: common.IDsNames{
|
||||||
IDToName: map[string]string{"foo": "bar"},
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
NameToID: map[string]string{"fnords": "smarf"},
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
},
|
},
|
||||||
|
rc: noLookup,
|
||||||
expectID: "",
|
expectID: "",
|
||||||
expectName: "",
|
expectName: "",
|
||||||
expectErr: require.Error,
|
expectErr: require.Error,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching maps with owner id and lookup",
|
||||||
|
owner: id,
|
||||||
|
ins: common.IDsNames{
|
||||||
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
|
},
|
||||||
|
rc: lookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching with owner name and lookup",
|
||||||
|
owner: name,
|
||||||
|
ins: common.IDsNames{
|
||||||
|
IDToName: map[string]string{"foo": "bar"},
|
||||||
|
NameToID: map[string]string{"fnords": "smarf"},
|
||||||
|
},
|
||||||
|
rc: lookup,
|
||||||
|
expectID: id,
|
||||||
|
expectName: name,
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
t = suite.T()
|
t = suite.T()
|
||||||
gc = &GraphConnector{}
|
gc = &GraphConnector{ownerLookup: test.rc}
|
||||||
)
|
)
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins)
|
rID, rName, err := gc.PopulateOwnerIDAndNamesFrom(ctx, test.owner, test.ins)
|
||||||
test.expectErr(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
assert.Equal(t, test.expectID, id)
|
assert.Equal(t, test.expectID, rID, "id")
|
||||||
assert.Equal(t, test.expectName, name)
|
assert.Equal(t, test.expectName, rName, "name")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1165,7 +1264,7 @@ func (suite *GraphConnectorIntegrationSuite) TestBackup_CreatesPrefixCollections
|
|||||||
start = time.Now()
|
start = time.Now()
|
||||||
)
|
)
|
||||||
|
|
||||||
id, name, err := backupGC.PopulateOwnerIDAndNamesFrom(backupSel.DiscreteOwner, nil)
|
id, name, err := backupGC.PopulateOwnerIDAndNamesFrom(ctx, backupSel.DiscreteOwner, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
backupSel.SetDiscreteOwnerIDName(id, name)
|
backupSel.SetDiscreteOwnerIDName(id, name)
|
||||||
|
|||||||
@ -123,7 +123,7 @@ func prepNewTestBackupOp(
|
|||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(sel.DiscreteOwner, nil)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(ctx, sel.DiscreteOwner, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
sel.SetDiscreteOwnerIDName(id, name)
|
sel.SetDiscreteOwnerIDName(id, name)
|
||||||
|
|||||||
@ -278,7 +278,7 @@ func setupExchangeBackup(
|
|||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(owner, nil)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(ctx, owner, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
bsel.DiscreteOwner = owner
|
bsel.DiscreteOwner = owner
|
||||||
@ -340,7 +340,7 @@ func setupSharePointBackup(
|
|||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
id, name, err := gc.PopulateOwnerIDAndNamesFrom(owner, nil)
|
id, name, err := gc.PopulateOwnerIDAndNamesFrom(ctx, owner, nil)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
spsel.DiscreteOwner = owner
|
spsel.DiscreteOwner = owner
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const (
|
|||||||
// M365 config
|
// M365 config
|
||||||
TestCfgAzureTenantID = "azure_tenantid"
|
TestCfgAzureTenantID = "azure_tenantid"
|
||||||
TestCfgSiteID = "m365siteid"
|
TestCfgSiteID = "m365siteid"
|
||||||
|
TestCfgSiteURL = "m365siteurl"
|
||||||
TestCfgUserID = "m365userid"
|
TestCfgUserID = "m365userid"
|
||||||
TestCfgSecondaryUserID = "secondarym365userid"
|
TestCfgSecondaryUserID = "secondarym365userid"
|
||||||
TestCfgLoadTestUserID = "loadtestm365userid"
|
TestCfgLoadTestUserID = "loadtestm365userid"
|
||||||
@ -34,6 +35,7 @@ const (
|
|||||||
// test specific env vars
|
// test specific env vars
|
||||||
const (
|
const (
|
||||||
EnvCorsoM365TestSiteID = "CORSO_M365_TEST_SITE_ID"
|
EnvCorsoM365TestSiteID = "CORSO_M365_TEST_SITE_ID"
|
||||||
|
EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL"
|
||||||
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
|
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
|
||||||
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
|
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
|
||||||
EnvCorsoM365LoadTestUserID = "CORSO_M365_LOAD_TEST_USER_ID"
|
EnvCorsoM365LoadTestUserID = "CORSO_M365_LOAD_TEST_USER_ID"
|
||||||
@ -136,6 +138,12 @@ func readTestConfig() (map[string]string, error) {
|
|||||||
os.Getenv(EnvCorsoM365TestSiteID),
|
os.Getenv(EnvCorsoM365TestSiteID),
|
||||||
vpr.GetString(TestCfgSiteID),
|
vpr.GetString(TestCfgSiteID),
|
||||||
"10rqc2.sharepoint.com,4892edf5-2ebf-46be-a6e5-a40b2cbf1c1a,38ab6d06-fc82-4417-af93-22d8733c22be")
|
"10rqc2.sharepoint.com,4892edf5-2ebf-46be-a6e5-a40b2cbf1c1a,38ab6d06-fc82-4417-af93-22d8733c22be")
|
||||||
|
fallbackTo(
|
||||||
|
testEnv,
|
||||||
|
TestCfgSiteURL,
|
||||||
|
os.Getenv(EnvCorsoM365TestSiteURL),
|
||||||
|
vpr.GetString(TestCfgSiteURL),
|
||||||
|
"https://10rqc2.sharepoint.com/sites/CorsoCI")
|
||||||
|
|
||||||
testEnv[EnvCorsoTestConfigFilePath] = os.Getenv(EnvCorsoTestConfigFilePath)
|
testEnv[EnvCorsoTestConfigFilePath] = os.Getenv(EnvCorsoTestConfigFilePath)
|
||||||
testConfig = testEnv
|
testConfig = testEnv
|
||||||
|
|||||||
@ -81,7 +81,6 @@ func LoadTestM365SiteID(t *testing.T) string {
|
|||||||
cfg, err := readTestConfig()
|
cfg, err := readTestConfig()
|
||||||
require.NoError(t, err, "retrieving load test m365 site id from test configuration", clues.ToCore(err))
|
require.NoError(t, err, "retrieving load test m365 site id from test configuration", clues.ToCore(err))
|
||||||
|
|
||||||
// TODO: load test site id, not standard test site id
|
|
||||||
return cfg[TestCfgSiteID]
|
return cfg[TestCfgSiteID]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,3 +161,14 @@ func M365SiteID(t *testing.T) string {
|
|||||||
|
|
||||||
return cfg[TestCfgSiteID]
|
return cfg[TestCfgSiteID]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// M365SiteURL returns a site webURL string representing the m365SiteURL described
|
||||||
|
// by either the env var CORSO_M365_TEST_SITE_URL, the corso_test.toml config
|
||||||
|
// file or the default value (in that order of priority). The default is a
|
||||||
|
// last-attempt fallback that will only work on alcion's testing org.
|
||||||
|
func M365SiteURL(t *testing.T) string {
|
||||||
|
cfg, err := readTestConfig()
|
||||||
|
require.NoError(t, err, "retrieving m365 site url from test configuration", clues.ToCore(err))
|
||||||
|
|
||||||
|
return cfg[TestCfgSiteURL]
|
||||||
|
}
|
||||||
|
|||||||
@ -314,7 +314,7 @@ func (r repository) NewBackupWithLookup(
|
|||||||
return operations.BackupOperation{}, errors.Wrap(err, "connecting to m365")
|
return operations.BackupOperation{}, errors.Wrap(err, "connecting to m365")
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerID, ownerName, err := gc.PopulateOwnerIDAndNamesFrom(sel.DiscreteOwner, ins)
|
ownerID, ownerName, err := gc.PopulateOwnerIDAndNamesFrom(ctx, sel.DiscreteOwner, ins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return operations.BackupOperation{}, errors.Wrap(err, "resolving resource owner details")
|
return operations.BackupOperation{}, errors.Wrap(err, "resolving resource owner details")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -198,7 +198,9 @@ func (suite *RepositoryIntegrationSuite) TestNewBackup() {
|
|||||||
r, err := repository.Initialize(ctx, acct, st, control.Options{})
|
r, err := repository.Initialize(ctx, acct, st, control.Options{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
bo, err := r.NewBackup(ctx, selectors.Selector{DiscreteOwner: "test"})
|
userID := tester.M365UserID(t)
|
||||||
|
|
||||||
|
bo, err := r.NewBackup(ctx, selectors.Selector{DiscreteOwner: userID})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
require.NotNil(t, bo)
|
require.NotNil(t, bo)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user