Switch to using ids for storing permissions (#2674)

This also enable backing up and restoring permissions for groups and applications.

---

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

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* fixes https://github.com/alcionai/corso/issues/2621
* fixes https://github.com/alcionai/corso/issues/2673

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [ ]  Unit test
- [x] 💚 E2E
This commit is contained in:
Abin Simon 2023-03-07 09:37:37 +05:30 committed by GitHub
parent 030147ddcb
commit 33ecbd1820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 100 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Show owner information when doing backup list in json format - Show owner information when doing backup list in json format
- Onedrive files that are flagged as malware get skipped during backup. - Onedrive files that are flagged as malware get skipped during backup.
- Permissions for groups can now be backed up and restored
### Fixed ### Fixed
- Corso-generated .meta files and permissions no longer appear in the backup details. - Corso-generated .meta files and permissions no longer appear in the backup details.

View File

@ -20,17 +20,27 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
func getMetadata(fileName, user string, roles []string) onedrive.Metadata { // For any version post this(inclusive), we expect to be using IDs for
if len(user) == 0 || len(roles) == 0 { // permission instead of email
const versionPermissionSwitchedToID = version.OneDrive4DirIncludesPermissions
func getMetadata(fileName string, perm permData, permUseID bool) onedrive.Metadata {
if len(perm.user) == 0 || len(perm.roles) == 0 {
return onedrive.Metadata{FileName: fileName} return onedrive.Metadata{FileName: fileName}
} }
id := base64.StdEncoding.EncodeToString([]byte(user + strings.Join(roles, "+"))) id := base64.StdEncoding.EncodeToString([]byte(perm.user + strings.Join(perm.roles, "+")))
uperm := onedrive.UserPermission{ID: id, Roles: perm.roles}
if permUseID {
uperm.EntityID = perm.entityID
} else {
uperm.Email = perm.user
}
testMeta := onedrive.Metadata{ testMeta := onedrive.Metadata{
FileName: fileName, FileName: fileName,
Permissions: []onedrive.UserPermission{ Permissions: []onedrive.UserPermission{uperm},
{ID: id, Roles: roles, Email: user},
},
} }
return testMeta return testMeta
@ -66,12 +76,12 @@ func onedriveItemWithData(
func onedriveMetadata( func onedriveMetadata(
t *testing.T, t *testing.T,
fileName, itemID string, fileName, itemID string,
user string, perm permData,
roles []string, permUseID bool,
) itemInfo { ) itemInfo {
t.Helper() t.Helper()
testMeta := getMetadata(fileName, user, roles) testMeta := getMetadata(fileName, perm, permUseID)
testMetaJSON, err := json.Marshal(testMeta) testMetaJSON, err := json.Marshal(testMeta)
require.NoError(t, err, "marshalling metadata") require.NoError(t, err, "marshalling metadata")
@ -85,10 +95,12 @@ func onedriveMetadata(
type GraphConnectorOneDriveIntegrationSuite struct { type GraphConnectorOneDriveIntegrationSuite struct {
tester.Suite tester.Suite
connector *GraphConnector connector *GraphConnector
user string user string
secondaryUser string userID string
acct account.Account secondaryUser string
secondaryUserID string
acct account.Account
} }
func TestGraphConnectorOneDriveIntegrationSuite(t *testing.T) { func TestGraphConnectorOneDriveIntegrationSuite(t *testing.T) {
@ -110,6 +122,14 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) SetupSuite() {
suite.secondaryUser = tester.SecondaryM365UserID(suite.T()) suite.secondaryUser = tester.SecondaryM365UserID(suite.T())
suite.acct = tester.NewM365Account(suite.T()) suite.acct = tester.NewM365Account(suite.T())
user, err := suite.connector.Owners.Users().GetByID(ctx, suite.user)
require.NoErrorf(suite.T(), err, "fetching user %s", suite.user)
suite.userID = *user.GetId()
secondaryUser, err := suite.connector.Owners.Users().GetByID(ctx, suite.secondaryUser)
require.NoErrorf(suite.T(), err, "fetching user %s", suite.secondaryUser)
suite.secondaryUserID = *secondaryUser.GetId()
tester.LogTimeOfTest(suite.T()) tester.LogTimeOfTest(suite.T())
} }
@ -157,12 +177,7 @@ func (c onedriveCollection) collection() colInfo {
} }
} }
func (c *onedriveCollection) withFile( func (c *onedriveCollection) withFile(name string, fileData []byte, perm permData) *onedriveCollection {
name string,
fileData []byte,
user string,
roles []string,
) *onedriveCollection {
switch c.backupVersion { switch c.backupVersion {
case 0: case 0:
// Lookups will occur using the most recent version of things so we need // Lookups will occur using the most recent version of things so we need
@ -184,8 +199,8 @@ func (c *onedriveCollection) withFile(
c.t, c.t,
"", "",
name+onedrive.MetaFileSuffix, name+onedrive.MetaFileSuffix,
user, perm,
roles) c.backupVersion >= versionPermissionSwitchedToID)
c.items = append(c.items, metadata) c.items = append(c.items, metadata)
c.aux = append(c.aux, metadata) c.aux = append(c.aux, metadata)
@ -196,11 +211,7 @@ func (c *onedriveCollection) withFile(
return c return c
} }
func (c *onedriveCollection) withFolder( func (c *onedriveCollection) withFolder(name string, perm permData) *onedriveCollection {
name string,
user string,
roles []string,
) *onedriveCollection {
switch c.backupVersion { switch c.backupVersion {
case 0, version.OneDrive4DirIncludesPermissions: case 0, version.OneDrive4DirIncludesPermissions:
return c return c
@ -212,8 +223,8 @@ func (c *onedriveCollection) withFolder(
c.t, c.t,
"", "",
name+onedrive.DirMetaFileSuffix, name+onedrive.DirMetaFileSuffix,
user, perm,
roles)) c.backupVersion >= versionPermissionSwitchedToID))
default: default:
assert.FailNowf(c.t, "bad backup version", "version %d", c.backupVersion) assert.FailNowf(c.t, "bad backup version", "version %d", c.backupVersion)
@ -224,10 +235,7 @@ func (c *onedriveCollection) withFolder(
// withPermissions adds permissions to the folder represented by this // withPermissions adds permissions to the folder represented by this
// onedriveCollection. // onedriveCollection.
func (c *onedriveCollection) withPermissions( func (c *onedriveCollection) withPermissions(perm permData) *onedriveCollection {
user string,
roles []string,
) *onedriveCollection {
// These versions didn't store permissions for the folder or didn't store them // These versions didn't store permissions for the folder or didn't store them
// in the folder's collection. // in the folder's collection.
if c.backupVersion < version.OneDrive4DirIncludesPermissions { if c.backupVersion < version.OneDrive4DirIncludesPermissions {
@ -244,8 +252,8 @@ func (c *onedriveCollection) withPermissions(
c.t, c.t,
name, name,
name+onedrive.DirMetaFileSuffix, name+onedrive.DirMetaFileSuffix,
user, perm,
roles) c.backupVersion >= versionPermissionSwitchedToID)
c.items = append(c.items, metadata) c.items = append(c.items, metadata)
c.aux = append(c.aux, metadata) c.aux = append(c.aux, metadata)
@ -254,8 +262,9 @@ func (c *onedriveCollection) withPermissions(
} }
type permData struct { type permData struct {
user string user string // user is only for older versions
roles []string entityID string
roles []string
} }
type itemData struct { type itemData struct {
@ -285,14 +294,14 @@ func testDataForInfo(t *testing.T, cols []onedriveColInfo, backupVersion int) []
onedriveCol := newOneDriveCollection(t, c.pathElements, backupVersion) onedriveCol := newOneDriveCollection(t, c.pathElements, backupVersion)
for _, f := range c.files { for _, f := range c.files {
onedriveCol.withFile(f.name, f.data, f.perms.user, f.perms.roles) onedriveCol.withFile(f.name, f.data, f.perms)
} }
for _, d := range c.folders { for _, d := range c.folders {
onedriveCol.withFolder(d.name, d.perms.user, d.perms.roles) onedriveCol.withFolder(d.name, d.perms)
} }
onedriveCol.withPermissions(c.perms.user, c.perms.roles) onedriveCol.withPermissions(c.perms)
res = append(res, onedriveCol.collection()) res = append(res, onedriveCol.collection())
} }
@ -503,8 +512,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
name: fileName, name: fileName,
data: fileAData, data: fileAData,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: writePerm, entityID: suite.secondaryUserID,
roles: writePerm,
}, },
}, },
{ {
@ -521,15 +531,17 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
{ {
name: folderAName, name: folderAName,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
{ {
name: folderCName, name: folderCName,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
}, },
@ -543,8 +555,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
name: fileName, name: fileName,
data: fileBData, data: fileBData,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
}, },
@ -552,8 +565,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
{ {
name: folderAName, name: folderAName,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
}, },
@ -567,14 +581,16 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
name: fileName, name: fileName,
data: fileDData, data: fileDData,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
}, },
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
{ {
@ -586,14 +602,16 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
name: fileName, name: fileName,
data: fileEData, data: fileEData,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: writePerm, entityID: suite.secondaryUserID,
roles: writePerm,
}, },
}, },
}, },
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
{ {
@ -607,8 +625,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
}, },
}, },
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: readPerm, entityID: suite.secondaryUserID,
roles: readPerm,
}, },
}, },
}, },
@ -619,6 +638,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndBa
for vn := test.startVersion; vn <= version.Backup; vn++ { for vn := test.startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("Version%d", vn), func() { suite.Run(fmt.Sprintf("Version%d", vn), func() {
t := suite.T() t := suite.T()
// Ideally this can always be true or false and still
// work, but limiting older versions to use emails so as
// to validate that flow as well.
input := testDataForInfo(t, test.cols, vn) input := testDataForInfo(t, test.cols, vn)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
@ -670,8 +692,9 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsBackupAndNoR
name: fileName, name: fileName,
data: fileAData, data: fileAData,
perms: permData{ perms: permData{
user: suite.secondaryUser, user: suite.secondaryUser,
roles: writePerm, entityID: suite.secondaryUserID,
roles: writePerm,
}, },
}, },
}, },
@ -742,6 +765,18 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
suite.user, suite.user,
) )
secondaryUserRead := permData{
user: suite.secondaryUser,
entityID: suite.secondaryUserID,
roles: readPerm,
}
secondaryUserWrite := permData{
user: suite.secondaryUser,
entityID: suite.secondaryUserID,
roles: writePerm,
}
test := restoreBackupInfoMultiVersion{ test := restoreBackupInfoMultiVersion{
service: path.OneDriveService, service: path.OneDriveService,
resource: Users, resource: Users,
@ -759,13 +794,11 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
withFile( withFile(
fileName, fileName,
fileAData, fileAData,
suite.secondaryUser, secondaryUserWrite,
writePerm,
). ).
withFolder( withFolder(
folderBName, folderBName,
suite.secondaryUser, secondaryUserRead,
readPerm,
). ).
collection(), collection(),
newOneDriveCollection( newOneDriveCollection(
@ -781,12 +814,10 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
withFile( withFile(
fileName, fileName,
fileEData, fileEData,
suite.secondaryUser, secondaryUserRead,
readPerm,
). ).
withPermissions( withPermissions(
suite.secondaryUser, secondaryUserRead,
readPerm,
). ).
collection(), collection(),
}, },
@ -803,13 +834,11 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
withFile( withFile(
fileName, fileName,
fileAData, fileAData,
"", permData{},
nil,
). ).
withFolder( withFolder(
folderBName, folderBName,
"", permData{},
nil,
). ).
collection(), collection(),
newOneDriveCollection( newOneDriveCollection(
@ -825,14 +854,12 @@ func (suite *GraphConnectorOneDriveIntegrationSuite) TestPermissionsRestoreAndNo
withFile( withFile(
fileName, fileName,
fileEData, fileEData,
"", permData{},
nil,
). ).
// Call this to generate a meta file with the folder name that we can // Call this to generate a meta file with the folder name that we can
// check. // check.
withPermissions( withPermissions(
"", permData{},
nil,
). ).
collection(), collection(),
}, },

View File

@ -204,7 +204,8 @@ func (oc Collection) DoNotMergeItems() bool {
type UserPermission struct { type UserPermission struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Roles []string `json:"role,omitempty"` Roles []string `json:"role,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"` // DEPRECATED: Replaced with UserID in newer backups
EntityID string `json:"entityId,omitempty"`
Expiration *time.Time `json:"expiration,omitempty"` Expiration *time.Time `json:"expiration,omitempty"`
} }

View File

@ -237,12 +237,12 @@ func oneDriveItemPermissionInfo(
return nil, err return nil, err
} }
uperms := filterUserPermissions(perm.GetValue()) uperms := filterUserPermissions(ctx, perm.GetValue())
return uperms, nil return uperms, nil
} }
func filterUserPermissions(perms []models.Permissionable) []UserPermission { func filterUserPermissions(ctx context.Context, perms []models.Permissionable) []UserPermission {
up := []UserPermission{} up := []UserPermission{}
for _, p := range perms { for _, p := range perms {
@ -252,6 +252,7 @@ func filterUserPermissions(perms []models.Permissionable) []UserPermission {
continue continue
} }
gv2 := p.GetGrantedToV2()
roles := []string{} roles := []string{}
for _, r := range p.GetRoles() { for _, r := range p.GetRoles() {
@ -265,10 +266,35 @@ func filterUserPermissions(perms []models.Permissionable) []UserPermission {
continue continue
} }
entityID := ""
if gv2.GetUser() != nil {
entityID = *gv2.GetUser().GetId()
} else if gv2.GetGroup() != nil {
entityID = *gv2.GetGroup().GetId()
} else {
// TODO Add appliction permissions when adding permissions for SharePoint
// https://devblogs.microsoft.com/microsoft365dev/controlling-app-access-on-specific-sharepoint-site-collections/
logm := logger.Ctx(ctx)
if gv2.GetApplication() != nil {
logm.With("application_id", *gv2.GetApplication().GetId())
}
if gv2.GetDevice() != nil {
logm.With("application_id", *gv2.GetDevice().GetId())
}
logm.Warn("untracked permission")
}
// Technically GrantedToV2 can also contain devices, but the
// documentation does not mention about devices in permissions
if entityID == "" {
// This should ideally not be hit
continue
}
up = append(up, UserPermission{ up = append(up, UserPermission{
ID: ptr.Val(p.GetId()), ID: ptr.Val(p.GetId()),
Roles: roles, Roles: roles,
Email: *p.GetGrantedToV2().GetUser().GetAdditionalData()["email"].(*string), EntityID: entityID,
Expiration: p.GetExpirationDateTime(), Expiration: p.GetExpirationDateTime(),
}) })
} }

View File

@ -235,12 +235,23 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
} }
} }
func getPermsUperms(permID, userID string, scopes []string) (models.Permissionable, UserPermission) { func getPermsUperms(permID, userID, entity string, scopes []string) (models.Permissionable, UserPermission) {
identity := models.NewIdentity() identity := models.NewIdentity()
identity.SetId(&userID)
identity.SetAdditionalData(map[string]any{"email": &userID}) identity.SetAdditionalData(map[string]any{"email": &userID})
sharepointIdentity := models.NewSharePointIdentitySet() sharepointIdentity := models.NewSharePointIdentitySet()
sharepointIdentity.SetUser(identity)
switch entity {
case "user":
sharepointIdentity.SetUser(identity)
case "group":
sharepointIdentity.SetGroup(identity)
case "application":
sharepointIdentity.SetApplication(identity)
case "device":
sharepointIdentity.SetDevice(identity)
}
perm := models.NewPermission() perm := models.NewPermission()
perm.SetId(&permID) perm.SetId(&permID)
@ -248,23 +259,34 @@ func getPermsUperms(permID, userID string, scopes []string) (models.Permissionab
perm.SetGrantedToV2(sharepointIdentity) perm.SetGrantedToV2(sharepointIdentity)
uperm := UserPermission{ uperm := UserPermission{
ID: permID, ID: permID,
Roles: []string{"read"}, Roles: []string{"read"},
Email: userID, EntityID: userID,
} }
return perm, uperm return perm, uperm
} }
func TestOneDrivePermissionsFilter(t *testing.T) { type ItemUnitTestSuite struct {
tester.Suite
}
func TestItemUnitTestSuite(t *testing.T) {
suite.Run(t, &ItemUnitTestSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ItemUnitTestSuite) TestOneDrivePermissionsFilter() {
permID := "fakePermId" permID := "fakePermId"
userID := "fakeuser@provider.com" userID := "fakeuser@provider.com"
userID2 := "fakeuser2@provider.com" userID2 := "fakeuser2@provider.com"
readPerm, readUperm := getPermsUperms(permID, userID, []string{"read"}) userReadPerm, userReadUperm := getPermsUperms(permID, userID, "user", []string{"read"})
readWritePerm, readWriteUperm := getPermsUperms(permID, userID2, []string{"read", "write"}) userReadWritePerm, userReadWriteUperm := getPermsUperms(permID, userID2, "user", []string{"read", "write"})
noPerm, _ := getPermsUperms(permID, userID, []string{"read"}) groupReadPerm, groupReadUperm := getPermsUperms(permID, userID, "group", []string{"read"})
groupReadWritePerm, groupReadWriteUperm := getPermsUperms(permID, userID2, "group", []string{"read", "write"})
noPerm, _ := getPermsUperms(permID, userID, "user", []string{"read"})
noPerm.SetGrantedToV2(nil) // eg: link shares noPerm.SetGrantedToV2(nil) // eg: link shares
cases := []struct { cases := []struct {
@ -282,24 +304,48 @@ func TestOneDrivePermissionsFilter(t *testing.T) {
graphPermissions: []models.Permissionable{noPerm}, graphPermissions: []models.Permissionable{noPerm},
parsedPermissions: []UserPermission{}, parsedPermissions: []UserPermission{},
}, },
// user
{ {
name: "user with read permissions", name: "user with read permissions",
graphPermissions: []models.Permissionable{readPerm}, graphPermissions: []models.Permissionable{userReadPerm},
parsedPermissions: []UserPermission{readUperm}, parsedPermissions: []UserPermission{userReadUperm},
}, },
{ {
name: "user with read and write permissions", name: "user with read and write permissions",
graphPermissions: []models.Permissionable{readWritePerm}, graphPermissions: []models.Permissionable{userReadWritePerm},
parsedPermissions: []UserPermission{readWriteUperm}, parsedPermissions: []UserPermission{userReadWriteUperm},
}, },
{ {
name: "multiple users with separate permissions", name: "multiple users with separate permissions",
graphPermissions: []models.Permissionable{readPerm, readWritePerm}, graphPermissions: []models.Permissionable{userReadPerm, userReadWritePerm},
parsedPermissions: []UserPermission{readUperm, readWriteUperm}, parsedPermissions: []UserPermission{userReadUperm, userReadWriteUperm},
},
// group
{
name: "group with read permissions",
graphPermissions: []models.Permissionable{groupReadPerm},
parsedPermissions: []UserPermission{groupReadUperm},
},
{
name: "group with read and write permissions",
graphPermissions: []models.Permissionable{groupReadWritePerm},
parsedPermissions: []UserPermission{groupReadWriteUperm},
},
{
name: "multiple groups with separate permissions",
graphPermissions: []models.Permissionable{groupReadPerm, groupReadWritePerm},
parsedPermissions: []UserPermission{groupReadUperm, groupReadWriteUperm},
}, },
} }
for _, tc := range cases { for _, tc := range cases {
actual := filterUserPermissions(tc.graphPermissions) suite.Run(tc.name, func() {
assert.ElementsMatch(t, tc.parsedPermissions, actual) ctx, flush := tester.NewContext()
defer flush()
actual := filterUserPermissions(ctx, tc.graphPermissions)
assert.ElementsMatch(suite.T(), tc.parsedPermissions, actual)
})
} }
} }

View File

@ -210,7 +210,14 @@ func restorePermissions(
pbody.SetRequireSignIn(&rs) pbody.SetRequireSignIn(&rs)
rec := models.NewDriveRecipient() rec := models.NewDriveRecipient()
rec.SetEmail(&p.Email) if p.EntityID != "" {
rec.SetObjectId(&p.EntityID)
} else {
// Previous versions used to only store email for a
// permissions. Use that if id is not found.
rec.SetEmail(&p.Email)
}
pbody.SetRecipients([]models.DriveRecipientable{rec}) pbody.SetRecipients([]models.DriveRecipientable{rec})
np, err := service.Client().DrivesById(driveID).ItemsById(itemID).Invite().Post(ctx, pbody, nil) np, err := service.Client().DrivesById(driveID).ItemsById(itemID).Invite().Post(ctx, pbody, nil)