add a weburl to siteid reducer in gc (#1671)

## Description

Adds a func in graphConnector that reduces
siteIDs and webURLs into a set of siteIDs.  This
will be used by callers such as the CLI to
generate id-based selectors for sites even if
they handle webURLs as an alternative id.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1616

## Test Plan

- [x]  Unit test
This commit is contained in:
Keepers 2022-12-08 08:38:24 -07:00 committed by GitHub
parent 99f35eb5a8
commit a6aa86ce5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 14 deletions

View File

@ -183,7 +183,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error {
sel := sharePointBackupCreateSelectors(site) sel := sharePointBackupCreateSelectors(site)
sites, err := m365.Sites(ctx, acct) sites, err := m365.SiteIDs(ctx, acct)
if err != nil { if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to retrieve SharePoint sites")) return Only(ctx, errors.Wrap(err, "Failed to retrieve SharePoint sites"))
} }

View File

@ -29,7 +29,7 @@ func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Se
ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String())) ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String()))
defer end() defer end()
err := verifyBackupInputs(sels, gc.GetUsers(), gc.GetSiteIds()) err := verifyBackupInputs(sels, gc.GetUsers(), gc.GetSiteIDs())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -40,7 +40,7 @@ func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Se
case selectors.ServiceOneDrive: case selectors.ServiceOneDrive:
return gc.OneDriveDataCollections(ctx, sels) return gc.OneDriveDataCollections(ctx, sels)
case selectors.ServiceSharePoint: case selectors.ServiceSharePoint:
colls, err := sharepoint.DataCollections(ctx, sels, gc.GetSiteIds(), gc.credentials.AzureTenantID, gc) colls, err := sharepoint.DataCollections(ctx, sels, gc.GetSiteIDs(), gc.credentials.AzureTenantID, gc)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"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/filters"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
@ -169,7 +170,7 @@ func (gc *GraphConnector) setTenantUsers(ctx context.Context) error {
return nil return nil
} }
// GetUsers returns the email address of users within tenant. // GetUsers returns the email address of users within the tenant.
func (gc *GraphConnector) GetUsers() []string { func (gc *GraphConnector) GetUsers() []string {
return buildFromMap(true, gc.Users) return buildFromMap(true, gc.Users)
} }
@ -218,7 +219,7 @@ func identifySite(item any) (string, string, error) {
} }
if m.GetName() == nil { if m.GetName() == nil {
// the built-in site at "htps://{tenant-domain}/search" never has a name. // the built-in site at "https://{tenant-domain}/search" never has a name.
if m.GetWebUrl() != nil && strings.HasSuffix(*m.GetWebUrl(), "/search") { if m.GetWebUrl() != nil && strings.HasSuffix(*m.GetWebUrl(), "/search") {
return "", "", errKnownSkippableCase return "", "", errKnownSkippableCase
} }
@ -232,19 +233,55 @@ func identifySite(item any) (string, string, error) {
return "", "", errKnownSkippableCase return "", "", errKnownSkippableCase
} }
return *m.GetName(), *m.GetId(), nil return *m.GetWebUrl(), *m.GetId(), nil
} }
// GetSites returns the siteIDs of sharepoint sites within tenant. // GetSiteWebURLs returns the WebURLs of sharepoint sites within the tenant.
func (gc *GraphConnector) GetSites() []string { func (gc *GraphConnector) GetSiteWebURLs() []string {
return buildFromMap(true, gc.Sites) return buildFromMap(true, gc.Sites)
} }
// GetSiteIds returns the M365 id for the user // GetSiteIds returns the canonical site IDs in the tenant
func (gc *GraphConnector) GetSiteIds() []string { func (gc *GraphConnector) GetSiteIDs() []string {
return buildFromMap(false, gc.Sites) return buildFromMap(false, gc.Sites)
} }
// UnionSiteIDsAndWebURLs reduces the id and url slices into a single slice of site IDs.
// WebURLs will run as a path-suffix style matcher. Callers may provide partial urls, though
// each element in the url must fully match. Ex: the webURL value "foo" will match "www.ex.com/foo",
// but not match "www.ex.com/foobar".
// The returned IDs are reduced to a set of unique values.
func (gc *GraphConnector) UnionSiteIDsAndWebURLs(ctx context.Context, ids, urls []string) ([]string, error) {
if len(gc.Sites) == 0 {
if err := gc.setTenantSites(ctx); err != nil {
return nil, err
}
}
idm := map[string]struct{}{}
for _, id := range ids {
idm[id] = struct{}{}
}
match := filters.PathSuffix(urls)
for url, id := range gc.Sites {
if !match.Compare(url) {
continue
}
idm[id] = struct{}{}
}
idsl := make([]string, 0, len(idm))
for id := range idm {
idsl = append(idsl, id)
}
return idsl, nil
}
// buildFromMap helper function for returning []string from map. // buildFromMap helper function for returning []string from map.
// Returns list of keys iff true; otherwise returns a list of values // Returns list of keys iff true; otherwise returns a list of values
func buildFromMap(isKey bool, mapping map[string]string) []string { func buildFromMap(isKey bool, mapping map[string]string) []string {

View File

@ -20,6 +20,115 @@ import (
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
type GraphConnectorUnitSuite struct {
suite.Suite
}
func TestGraphConnectorUnitSuite(t *testing.T) {
suite.Run(t, new(GraphConnectorUnitSuite))
}
func (suite *GraphConnectorUnitSuite) TestUnionSiteIDsAndWebURLs() {
const (
url1 = "www.foo.com/bar"
url2 = "www.fnords.com/smarf"
path1 = "bar"
path2 = "/smarf"
id1 = "site-id-1"
id2 = "site-id-2"
)
gc := &GraphConnector{
// must be populated, else the func will try to make a graph call
// to retrieve site data.
Sites: map[string]string{
url1: id1,
url2: id2,
},
}
table := []struct {
name string
ids []string
urls []string
expect []string
}{
{
name: "nil",
},
{
name: "empty",
ids: []string{},
urls: []string{},
expect: []string{},
},
{
name: "ids only",
ids: []string{id1, id2},
urls: []string{},
expect: []string{id1, id2},
},
{
name: "urls only",
ids: []string{},
urls: []string{url1, url2},
expect: []string{id1, id2},
},
{
name: "url suffix only",
ids: []string{},
urls: []string{path1, path2},
expect: []string{id1, id2},
},
{
name: "url and suffix overlap",
ids: []string{},
urls: []string{url1, url2, path1, path2},
expect: []string{id1, id2},
},
{
name: "ids and urls, no overlap",
ids: []string{id1},
urls: []string{url2},
expect: []string{id1, id2},
},
{
name: "ids and urls, overlap",
ids: []string{id1, id2},
urls: []string{url1, url2},
expect: []string{id1, id2},
},
{
name: "partial non-match on path",
ids: []string{},
urls: []string{path1[2:], path2[2:]},
expect: []string{},
},
{
name: "partial non-match on url",
ids: []string{},
urls: []string{url1[5:], url2[5:]},
expect: []string{},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
//nolint
result, err := gc.UnionSiteIDsAndWebURLs(context.Background(), test.ids, test.urls)
assert.NoError(t, err)
assert.ElementsMatch(t, test.expect, result)
})
}
}
// ---------------------------------------------------------------------------
// Integration tests
// ---------------------------------------------------------------------------
type GraphConnectorIntegrationSuite struct { type GraphConnectorIntegrationSuite struct {
suite.Suite suite.Suite
connector *GraphConnector connector *GraphConnector

View File

@ -58,15 +58,24 @@ func UserIDs(ctx context.Context, m365Account account.Account) ([]string, error)
return ret, nil return ret, nil
} }
// Sites returns a list of SharePoint sites in the specified M365 tenant // SiteURLs returns a list of SharePoint site WebURLs in the specified M365 tenant
// TODO: Implement paging support func SiteURLs(ctx context.Context, m365Account account.Account) ([]string, error) {
func Sites(ctx context.Context, m365Account account.Account) ([]string, error) {
gc, err := connector.NewGraphConnector(ctx, m365Account, connector.Sites) gc, err := connector.NewGraphConnector(ctx, m365Account, connector.Sites)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not initialize M365 graph connection") return nil, errors.Wrap(err, "could not initialize M365 graph connection")
} }
return gc.GetSites(), nil return gc.GetSiteWebURLs(), nil
}
// SiteURLs returns a list of SharePoint sites IDs in the specified M365 tenant
func SiteIDs(ctx context.Context, m365Account account.Account) ([]string, error) {
gc, err := connector.NewGraphConnector(ctx, m365Account, connector.Sites)
if err != nil {
return nil, errors.Wrap(err, "could not initialize M365 graph connection")
}
return gc.GetSiteIDs(), nil
} }
// parseUser extracts information from `models.Userable` we care about // parseUser extracts information from `models.Userable` we care about