use display name to look up groups (#4242)

Allows lookup of groups using their display name in addition to their ID.

---

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

- [x]  No

#### Type of change

- [x] 🌻 Feature

#### Issue(s)

* #3988 

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-09-14 11:03:14 -06:00 committed by GitHub
parent edf753382e
commit cb319bb2ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 94 additions and 32 deletions

View File

@ -96,6 +96,13 @@ var (
// when filenames collide in a @microsoft.graph.conflictBehavior=fail request.
ErrItemAlreadyExistsConflict = clues.New("item already exists")
// ErrMultipleResultsMatchIdentifier describes a situation where we're doing a lookup
// in some way other than by canonical url ID (ex: filtering, searching, etc).
// This error should only be returned if a unique result is an expected constraint
// of the call results. If it's possible to opportunistically select one of the many
// replies, no error should get returned.
ErrMultipleResultsMatchIdentifier = clues.New("multiple results match the identifier")
// 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")

View File

@ -141,7 +141,7 @@ func (c Contacts) GetContainerByName(
// Return an error if multiple container exist (unlikely) or if no container
// is found.
if len(gv) != 1 {
return nil, clues.New("unexpected number of folders returned").
return nil, clues.Stack(graph.ErrMultipleResultsMatchIdentifier).
With("returned_container_count", len(gv)).
WithClues(ctx)
}

View File

@ -2,9 +2,11 @@ package api
import (
"context"
"fmt"
"github.com/alcionai/clues"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/groups"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
@ -70,7 +72,7 @@ func getGroups(
}
var (
groups = make([]models.Groupable, 0)
results = make([]models.Groupable, 0)
el = errs.Local()
)
@ -83,7 +85,7 @@ func getGroups(
if err != nil {
el.AddRecoverable(ctx, graph.Wrap(ctx, err, "validating groups"))
} else {
groups = append(groups, item)
results = append(results, item)
}
return true
@ -93,9 +95,11 @@ func getGroups(
return nil, graph.Wrap(ctx, err, "iterating all groups")
}
return groups, el.Failure()
return results, el.Failure()
}
const filterGroupByDisplayNameQueryTmpl = "displayName eq '%s'"
// GetID retrieves group by groupID.
func (c Groups) GetByID(
ctx context.Context,
@ -106,14 +110,49 @@ func (c Groups) GetByID(
return nil, err
}
resp, err := service.Client().Groups().ByGroupIdString(identifier).Get(ctx, nil)
if err != nil {
err := graph.Wrap(ctx, err, "getting group by id")
ctx = clues.Add(ctx, "resource_identifier", identifier)
return nil, err
var group models.Groupable
// prefer lookup by id, but fallback to lookup by display name,
// even in the case of a uuid, just in case the display name itself
// is a uuid.
if uuidRE.MatchString(identifier) {
group, err = service.
Client().
Groups().
ByGroupIdString(identifier).
Get(ctx, nil)
if err == nil {
return group, nil
}
return resp, graph.Stack(ctx, err).OrNil()
logger.CtxErr(ctx, err).Info("finding group by id, falling back to display name")
}
opts := &groups.GroupsRequestBuilderGetRequestConfiguration{
Headers: newEventualConsistencyHeaders(),
QueryParameters: &groups.GroupsRequestBuilderGetQueryParameters{
Filter: ptr.To(fmt.Sprintf(filterGroupByDisplayNameQueryTmpl, identifier)),
},
}
resp, err := service.Client().Groups().Get(ctx, opts)
if err != nil {
return nil, graph.Wrap(ctx, err, "finding group by display name")
}
vs := resp.GetValue()
if len(vs) == 0 {
return nil, clues.Stack(graph.ErrResourceOwnerNotFound).WithClues(ctx)
} else if len(vs) > 1 {
return nil, clues.Stack(graph.ErrMultipleResultsMatchIdentifier).WithClues(ctx)
}
group = vs[0]
return group, nil
}
// GetRootSite retrieves the root site for the group.
@ -158,10 +197,10 @@ func ValidateGroup(item models.Groupable) error {
return nil
}
func OnlyTeams(ctx context.Context, groups []models.Groupable) []models.Groupable {
func OnlyTeams(ctx context.Context, gs []models.Groupable) []models.Groupable {
var teams []models.Groupable
for _, g := range groups {
for _, g := range gs {
if IsTeam(ctx, g) {
teams = append(teams, g)
}

View File

@ -33,7 +33,7 @@ func (suite *GroupUnitSuite) TestValidateGroup() {
tests := []struct {
name string
args models.Groupable
errCheck assert.ErrorAssertionFunc
expectErr assert.ErrorAssertionFunc
errIsSkippable bool
}{
{
@ -44,7 +44,7 @@ func (suite *GroupUnitSuite) TestValidateGroup() {
s.SetDisplayName(ptr.To("testgroup"))
return s
}(),
errCheck: assert.NoError,
expectErr: assert.NoError,
},
{
name: "No name",
@ -53,7 +53,7 @@ func (suite *GroupUnitSuite) TestValidateGroup() {
s.SetId(ptr.To("id"))
return s
}(),
errCheck: assert.Error,
expectErr: assert.Error,
},
{
name: "No ID",
@ -62,7 +62,7 @@ func (suite *GroupUnitSuite) TestValidateGroup() {
s.SetDisplayName(ptr.To("testgroup"))
return s
}(),
errCheck: assert.Error,
expectErr: assert.Error,
},
}
@ -71,7 +71,7 @@ func (suite *GroupUnitSuite) TestValidateGroup() {
t := suite.T()
err := api.ValidateGroup(test.args)
test.errCheck(t, err, clues.ToCore(err))
test.expectErr(t, err, clues.ToCore(err))
if test.errIsSkippable {
assert.ErrorIs(t, err, api.ErrKnownSkippableCase)
@ -111,29 +111,43 @@ func (suite *GroupsIntgSuite) TestGetAll() {
}
func (suite *GroupsIntgSuite) TestGroups_GetByID() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
groupID = suite.its.group.id
groupsAPI = suite.its.ac.Groups()
)
grp, err := groupsAPI.GetByID(ctx, groupID)
require.NoError(t, err, clues.ToCore(err))
table := []struct {
name string
id string
expectErr func(*testing.T, error)
expectErr assert.ErrorAssertionFunc
}{
{
name: "valid id",
id: groupID,
expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err))
},
expectErr: assert.NoError,
},
{
name: "invalid id",
id: uuid.NewString(),
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
expectErr: assert.Error,
},
{
name: "valid display name",
id: ptr.Val(grp.GetDisplayName()),
expectErr: assert.NoError,
},
{
name: "invalid displayName",
id: "jabberwocky",
expectErr: assert.Error,
},
}
for _, test := range table {
@ -144,7 +158,7 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
defer flush()
_, err := groupsAPI.GetByID(ctx, test.id)
test.expectErr(t, err)
test.expectErr(t, err, clues.ToCore(err))
})
}
}

View File

@ -169,7 +169,7 @@ func (c Mail) GetContainerByName(
// Return an error if multiple container exist (unlikely) or if no container
// is found.
if len(gv) != 1 {
return nil, clues.New("unexpected number of folders returned").
return nil, clues.Stack(graph.ErrMultipleResultsMatchIdentifier).
With("returned_container_count", len(gv)).
WithClues(ctx)
}

View File

@ -102,12 +102,14 @@ func (c Sites) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable,
return us, el.Failure()
}
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}"
const uuidRETmpl = "[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}"
var uuidRE = regexp.MustCompile(uuidRETmpl)
// 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)
var siteIDRE = regexp.MustCompile(`(.+,)?` + uuidRETmpl + "," + uuidRETmpl)
const sitesWebURLGetTemplate = "https://graph.microsoft.com/v1.0/sites/%s:/%s?$expand=drive"