Fixing handling of permissions for external users (#5097)
<!-- PR description--> --- #### 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: ---> - [ ] 🌻 Feature - [x] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 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/5046 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
c526ced23e
commit
214d8a6030
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Backup attachments associated with group mailbox items.
|
- Backup attachments associated with group mailbox items.
|
||||||
- Groups and Teams backups no longer fail when a resource has no display name.
|
- Groups and Teams backups no longer fail when a resource has no display name.
|
||||||
- Contacts in-place restore failed if the restore destination was empty.
|
- Contacts in-place restore failed if the restore destination was empty.
|
||||||
|
- Link shares with external users are now backed up and restored as expected
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- When running `backup details` on an empty backup returns a more helpful error message.
|
- When running `backup details` on an empty backup returns a more helpful error message.
|
||||||
@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Event description for exchange exports might look slightly different for certain events.
|
- Event description for exchange exports might look slightly different for certain events.
|
||||||
- Exchange in-place restore may restore items in well-known folders to different folders if the user has well-known folder names change based on locale and has updated the locale since the backup was created.
|
- Exchange in-place restore may restore items in well-known folders to different folders if the user has well-known folder names change based on locale and has updated the locale since the backup was created.
|
||||||
- In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder.
|
- In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder.
|
||||||
|
- External users with access through shared links will not receive these links as they are not sent via email during restore.
|
||||||
|
|
||||||
## [v0.18.0] (beta) - 2024-01-02
|
## [v0.18.0] (beta) - 2024-01-02
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Entity struct {
|
type Entity struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
|
// Email is necessary when the user is external to the
|
||||||
|
// organization and we do not have an ID associated with the user.
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
|
||||||
EntityType GV2Type `json:"entityType,omitempty"`
|
EntityType GV2Type `json:"entityType,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/str"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,20 +43,32 @@ type Permission struct {
|
|||||||
|
|
||||||
// Equal checks equality of two UserPermission objects
|
// Equal checks equality of two UserPermission objects
|
||||||
func (p Permission) Equals(other Permission) bool {
|
func (p Permission) Equals(other Permission) bool {
|
||||||
// EntityID can be empty for older backups and Email can be empty
|
if p.EntityType != other.EntityType {
|
||||||
// for newer ones. It is not possible for both to be empty. Also,
|
|
||||||
// if EntityID/Email for one is not empty then the other will also
|
|
||||||
// have EntityID/Email as we backup permissions for all the
|
|
||||||
// parents and children when we have a change in permissions.
|
|
||||||
// We cannot just compare id because of the problem described in #3117
|
|
||||||
if len(p.EntityID) > 0 && p.EntityID != other.EntityID {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.Email) > 0 && p.Email != other.Email {
|
// NOTE: v1 of permissions only contain emails, v2 only contains IDs.
|
||||||
|
// The current one will contain both ID and email.
|
||||||
|
if len(p.EntityID) > 0 && len(other.EntityID) > 0 &&
|
||||||
|
p.EntityID != other.EntityID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In cases where we have shared an item with an external user,
|
||||||
|
// the user will not have an id
|
||||||
|
if len(p.EntityID) == 0 && len(other.EntityID) == 0 {
|
||||||
|
if len(p.Email) > 0 && len(other.Email) > 0 &&
|
||||||
|
p.Email != other.Email {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible that one is empty and the other is not
|
||||||
|
if p.EntityID != other.EntityID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot just compare id/email because of #3117
|
||||||
p1r := p.Roles
|
p1r := p.Roles
|
||||||
p2r := other.Roles
|
p2r := other.Roles
|
||||||
|
|
||||||
@ -166,20 +179,20 @@ func FilterPermissions(ctx context.Context, perms []models.Permissionable) []Per
|
|||||||
// https://devblogs.microsoft.com/microsoft365dev/controlling-app-access-on-specific-sharepoint-site-collections/
|
// https://devblogs.microsoft.com/microsoft365dev/controlling-app-access-on-specific-sharepoint-site-collections/
|
||||||
roles := p.GetRoles()
|
roles := p.GetRoles()
|
||||||
|
|
||||||
gv2t, entityID := getIdentityDetails(ctx, p.GetGrantedToV2())
|
ent, ok := getIdentityDetails(ctx, p.GetGrantedToV2())
|
||||||
|
if !ok {
|
||||||
// Technically GrantedToV2 can also contain devices, but the
|
// We log the inability to handle certain type of
|
||||||
// documentation does not mention about devices in permissions
|
// permissions within the getIdentityDetails function and so
|
||||||
if len(entityID) == 0 {
|
// we just skip here
|
||||||
// This should ideally not be hit
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
up = append(up, Permission{
|
up = append(up, Permission{
|
||||||
ID: ptr.Val(p.GetId()),
|
ID: ptr.Val(p.GetId()),
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
EntityID: entityID,
|
EntityID: ent.ID,
|
||||||
EntityType: gv2t,
|
Email: ent.Email, // not necessary if we have ID, but useful for debugging
|
||||||
|
EntityType: ent.EntityType,
|
||||||
Expiration: p.GetExpirationDateTime(),
|
Expiration: p.GetExpirationDateTime(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -205,16 +218,12 @@ func FilterLinkShares(ctx context.Context, perms []models.Permissionable) []Link
|
|||||||
idens := []Entity{}
|
idens := []Entity{}
|
||||||
|
|
||||||
for _, g := range gv2 {
|
for _, g := range gv2 {
|
||||||
gv2t, entityID := getIdentityDetails(ctx, g)
|
ent, ok := getIdentityDetails(ctx, g)
|
||||||
|
if !ok {
|
||||||
// Technically GrantedToV2 can also contain devices, but the
|
|
||||||
// documentation does not mention about devices in permissions
|
|
||||||
if len(entityID) == 0 {
|
|
||||||
// This should ideally not be hit
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
idens = append(idens, Entity{ID: entityID, EntityType: gv2t})
|
idens = append(idens, ent)
|
||||||
}
|
}
|
||||||
|
|
||||||
up = append(up, LinkShare{
|
up = append(up, LinkShare{
|
||||||
@ -235,34 +244,44 @@ func FilterLinkShares(ctx context.Context, perms []models.Permissionable) []Link
|
|||||||
return up
|
return up
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIdentityDetails(ctx context.Context, gv2 models.SharePointIdentitySetable) (GV2Type, string) {
|
func getIdentityDetails(ctx context.Context, gv2 models.SharePointIdentitySetable) (Entity, bool) {
|
||||||
var (
|
switch {
|
||||||
gv2t GV2Type
|
|
||||||
entityID string
|
|
||||||
)
|
|
||||||
|
|
||||||
switch true {
|
|
||||||
case gv2.GetUser() != nil:
|
case gv2.GetUser() != nil:
|
||||||
gv2t = GV2User
|
add := gv2.GetUser().GetAdditionalData()
|
||||||
entityID = ptr.Val(gv2.GetUser().GetId())
|
email, _ := str.AnyToString(add["email"]) // empty will be dropped automatically when writing
|
||||||
|
|
||||||
|
return Entity{
|
||||||
|
ID: ptr.Val(gv2.GetUser().GetId()),
|
||||||
|
Email: email,
|
||||||
|
EntityType: GV2User,
|
||||||
|
}, true
|
||||||
case gv2.GetSiteUser() != nil:
|
case gv2.GetSiteUser() != nil:
|
||||||
gv2t = GV2SiteUser
|
return Entity{
|
||||||
entityID = ptr.Val(gv2.GetSiteUser().GetId())
|
ID: ptr.Val(gv2.GetSiteUser().GetId()),
|
||||||
|
EntityType: GV2SiteUser,
|
||||||
|
}, true
|
||||||
case gv2.GetGroup() != nil:
|
case gv2.GetGroup() != nil:
|
||||||
gv2t = GV2Group
|
return Entity{
|
||||||
entityID = ptr.Val(gv2.GetGroup().GetId())
|
ID: ptr.Val(gv2.GetGroup().GetId()),
|
||||||
|
EntityType: GV2Group,
|
||||||
|
}, true
|
||||||
case gv2.GetSiteGroup() != nil:
|
case gv2.GetSiteGroup() != nil:
|
||||||
gv2t = GV2SiteGroup
|
return Entity{
|
||||||
entityID = ptr.Val(gv2.GetSiteGroup().GetId())
|
ID: ptr.Val(gv2.GetSiteGroup().GetId()),
|
||||||
|
EntityType: GV2SiteGroup,
|
||||||
|
}, true
|
||||||
case gv2.GetApplication() != nil:
|
case gv2.GetApplication() != nil:
|
||||||
gv2t = GV2App
|
return Entity{
|
||||||
entityID = ptr.Val(gv2.GetApplication().GetId())
|
ID: ptr.Val(gv2.GetApplication().GetId()),
|
||||||
|
EntityType: GV2App,
|
||||||
|
}, true
|
||||||
case gv2.GetDevice() != nil:
|
case gv2.GetDevice() != nil:
|
||||||
gv2t = GV2Device
|
return Entity{
|
||||||
entityID = ptr.Val(gv2.GetDevice().GetId())
|
ID: ptr.Val(gv2.GetDevice().GetId()),
|
||||||
|
EntityType: GV2Device,
|
||||||
|
}, true
|
||||||
default:
|
default:
|
||||||
logger.Ctx(ctx).Info("untracked permission")
|
logger.Ctx(ctx).Info("untracked permission")
|
||||||
|
return Entity{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return gv2t, entityID
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -225,10 +226,14 @@ func getPermsAndResourceOwnerPerms(
|
|||||||
case GV2App, GV2Device, GV2Group, GV2User:
|
case GV2App, GV2Device, GV2Group, GV2User:
|
||||||
identity := models.NewIdentity()
|
identity := models.NewIdentity()
|
||||||
identity.SetId(&resourceOwner)
|
identity.SetId(&resourceOwner)
|
||||||
identity.SetAdditionalData(map[string]any{"email": &resourceOwner})
|
|
||||||
|
|
||||||
switch gv2t {
|
switch gv2t {
|
||||||
case GV2User:
|
case GV2User:
|
||||||
|
// user's need to handle email
|
||||||
|
identity := models.NewUser()
|
||||||
|
identity.SetId(&resourceOwner)
|
||||||
|
identity.SetAdditionalData(map[string]any{"email": &resourceOwner})
|
||||||
|
|
||||||
sharepointIdentitySet.SetUser(identity)
|
sharepointIdentitySet.SetUser(identity)
|
||||||
case GV2Group:
|
case GV2Group:
|
||||||
sharepointIdentitySet.SetGroup(identity)
|
sharepointIdentitySet.SetGroup(identity)
|
||||||
@ -241,7 +246,6 @@ func getPermsAndResourceOwnerPerms(
|
|||||||
case GV2SiteUser, GV2SiteGroup:
|
case GV2SiteUser, GV2SiteGroup:
|
||||||
spIdentity := models.NewSharePointIdentity()
|
spIdentity := models.NewSharePointIdentity()
|
||||||
spIdentity.SetId(&resourceOwner)
|
spIdentity.SetId(&resourceOwner)
|
||||||
spIdentity.SetAdditionalData(map[string]any{"email": &resourceOwner})
|
|
||||||
|
|
||||||
switch gv2t {
|
switch gv2t {
|
||||||
case GV2SiteUser:
|
case GV2SiteUser:
|
||||||
@ -263,6 +267,11 @@ func getPermsAndResourceOwnerPerms(
|
|||||||
EntityType: gv2t,
|
EntityType: gv2t,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gv2t == GV2User {
|
||||||
|
// we currently don't parse out emails for others
|
||||||
|
ownersPerm.Email = resourceOwner
|
||||||
|
}
|
||||||
|
|
||||||
return perm, ownersPerm
|
return perm, ownersPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,3 +406,315 @@ func (suite *PermissionsUnitTestSuite) TestDrivePermissionsFilter() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsUnitTestSuite) TestEqual() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
perm1 Permission
|
||||||
|
perm2 Permission
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "same id no email",
|
||||||
|
perm1: Permission{
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityID: "user-id1",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityID: "user-id1",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no id same email",
|
||||||
|
perm1: Permission{
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Can happen if user changes email
|
||||||
|
name: "same id different email",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1-new@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different id different email",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
EntityID: "user-id2",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id2@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different id same email",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
EntityID: "user-id2",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one with id one with email",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Email: "id2@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same email one with no id",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// should not ideally happen, not entirely sure if it
|
||||||
|
// should be false as we could just be missing the id
|
||||||
|
name: "same email one with no id",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same id different role",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"write"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same email different role",
|
||||||
|
perm1: Permission{
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
Email: "id1@provider.com",
|
||||||
|
Roles: []string{"write"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same id different entity type",
|
||||||
|
perm1: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2User,
|
||||||
|
},
|
||||||
|
perm2: Permission{
|
||||||
|
EntityID: "user-id1",
|
||||||
|
Roles: []string{"read"},
|
||||||
|
EntityType: GV2Group,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, test.perm1.Equals(test.perm2), "permissions equality")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsUnitTestSuite) TestFilterLinkShares() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
perms func() []models.Permissionable
|
||||||
|
expected [][]Entity
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with id and email",
|
||||||
|
perms: func() []models.Permissionable {
|
||||||
|
perm1 := models.NewPermission()
|
||||||
|
perm1.SetId(ptr.To("id1"))
|
||||||
|
perm1.SetRoles([]string{"read"})
|
||||||
|
|
||||||
|
spi11 := models.NewUser()
|
||||||
|
spi11.SetId(ptr.To("user-id1"))
|
||||||
|
spi11.SetAdditionalData(map[string]any{"email": ptr.To("id1@provider")})
|
||||||
|
|
||||||
|
spi12 := models.NewUser()
|
||||||
|
spi12.SetId(ptr.To("user-id2"))
|
||||||
|
spi12.SetAdditionalData(map[string]any{"email": ptr.To("id2@provider")})
|
||||||
|
|
||||||
|
gv21 := models.NewSharePointIdentitySet()
|
||||||
|
gv21.SetUser(spi11)
|
||||||
|
|
||||||
|
gv22 := models.NewSharePointIdentitySet()
|
||||||
|
gv22.SetUser(spi12)
|
||||||
|
|
||||||
|
perm1.SetGrantedToIdentitiesV2([]models.SharePointIdentitySetable{gv21, gv22})
|
||||||
|
|
||||||
|
li1 := models.NewSharingLink()
|
||||||
|
li1.SetWebUrl(ptr.To("https://link1"))
|
||||||
|
perm1.SetLink(li1)
|
||||||
|
|
||||||
|
return []models.Permissionable{perm1}
|
||||||
|
},
|
||||||
|
expected: [][]Entity{
|
||||||
|
{
|
||||||
|
{ID: "user-id1", Email: "id1@provider", EntityType: GV2User},
|
||||||
|
{ID: "user-id2", Email: "id2@provider", EntityType: GV2User},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only email",
|
||||||
|
perms: func() []models.Permissionable {
|
||||||
|
perm1 := models.NewPermission()
|
||||||
|
perm1.SetId(ptr.To("id1"))
|
||||||
|
perm1.SetRoles([]string{"read"})
|
||||||
|
|
||||||
|
spi11 := models.NewUser()
|
||||||
|
spi11.SetAdditionalData(map[string]any{"email": ptr.To("id1@provider")})
|
||||||
|
|
||||||
|
spi12 := models.NewUser()
|
||||||
|
spi12.SetAdditionalData(map[string]any{"email": ptr.To("id2@provider")})
|
||||||
|
|
||||||
|
gv21 := models.NewSharePointIdentitySet()
|
||||||
|
gv21.SetUser(spi11)
|
||||||
|
|
||||||
|
gv22 := models.NewSharePointIdentitySet()
|
||||||
|
gv22.SetUser(spi12)
|
||||||
|
|
||||||
|
perm1.SetGrantedToIdentitiesV2([]models.SharePointIdentitySetable{gv21, gv22})
|
||||||
|
|
||||||
|
li1 := models.NewSharingLink()
|
||||||
|
li1.SetWebUrl(ptr.To("https://link1"))
|
||||||
|
perm1.SetLink(li1)
|
||||||
|
|
||||||
|
return []models.Permissionable{perm1}
|
||||||
|
},
|
||||||
|
expected: [][]Entity{
|
||||||
|
{
|
||||||
|
{Email: "id1@provider", EntityType: GV2User},
|
||||||
|
{Email: "id2@provider", EntityType: GV2User},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one with id one with email",
|
||||||
|
perms: func() []models.Permissionable {
|
||||||
|
perm1 := models.NewPermission()
|
||||||
|
perm1.SetId(ptr.To("id1"))
|
||||||
|
perm1.SetRoles([]string{"read"})
|
||||||
|
|
||||||
|
spi11 := models.NewUser()
|
||||||
|
spi11.SetId(ptr.To("user-id1"))
|
||||||
|
|
||||||
|
spi12 := models.NewUser()
|
||||||
|
spi12.SetAdditionalData(map[string]any{"email": ptr.To("id2@provider")})
|
||||||
|
|
||||||
|
gv21 := models.NewSharePointIdentitySet()
|
||||||
|
gv21.SetUser(spi11)
|
||||||
|
|
||||||
|
gv22 := models.NewSharePointIdentitySet()
|
||||||
|
gv22.SetUser(spi12)
|
||||||
|
|
||||||
|
perm1.SetGrantedToIdentitiesV2([]models.SharePointIdentitySetable{gv21, gv22})
|
||||||
|
|
||||||
|
li1 := models.NewSharingLink()
|
||||||
|
li1.SetWebUrl(ptr.To("https://link1"))
|
||||||
|
perm1.SetLink(li1)
|
||||||
|
|
||||||
|
return []models.Permissionable{perm1}
|
||||||
|
},
|
||||||
|
expected: [][]Entity{
|
||||||
|
{
|
||||||
|
{ID: "user-id1", EntityType: GV2User},
|
||||||
|
{Email: "id2@provider", EntityType: GV2User},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
actual := FilterLinkShares(ctx, test.perms())
|
||||||
|
assert.Equal(t, len(test.expected), len(actual), "number of link shares")
|
||||||
|
|
||||||
|
for i, expected := range test.expected {
|
||||||
|
assert.ElementsMatch(t, expected, actual[i].Entities, "link share entities")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package drive
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
||||||
@ -208,7 +207,8 @@ func UpdatePermissions(
|
|||||||
ictx := clues.Add(
|
ictx := clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
"permission_entity_type", p.EntityType,
|
"permission_entity_type", p.EntityType,
|
||||||
"permission_entity_id", clues.Hide(p.EntityID))
|
"permission_entity_id", clues.Hide(p.EntityID),
|
||||||
|
"permission_entity_email", clues.Hide(p.Email))
|
||||||
|
|
||||||
// deletes require unique http clients
|
// deletes require unique http clients
|
||||||
// https://github.com/alcionai/corso/issues/2707
|
// https://github.com/alcionai/corso/issues/2707
|
||||||
@ -245,7 +245,8 @@ func UpdatePermissions(
|
|||||||
ictx := clues.Add(
|
ictx := clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
"permission_entity_type", p.EntityType,
|
"permission_entity_type", p.EntityType,
|
||||||
"permission_entity_id", clues.Hide(p.EntityID))
|
"permission_entity_id", clues.Hide(p.EntityID),
|
||||||
|
"permission_entity_email", clues.Hide(p.Email))
|
||||||
|
|
||||||
// We are not able to restore permissions when there are no
|
// We are not able to restore permissions when there are no
|
||||||
// roles or for owner, this seems to be restriction in graph
|
// roles or for owner, this seems to be restriction in graph
|
||||||
@ -351,11 +352,16 @@ func UpdateLinkShares(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Using DriveRecipient seems to error out on Graph end
|
// Using DriveRecipient seems to error out on Graph end
|
||||||
idens = append(idens, map[string]string{"objectId": iden.ID})
|
if len(iden.ID) > 0 {
|
||||||
entities = append(entities, iden.ID)
|
idens = append(idens, map[string]string{"objectId": iden.ID})
|
||||||
|
entities = append(entities, iden.ID)
|
||||||
|
} else {
|
||||||
|
idens = append(idens, map[string]string{"email": iden.Email})
|
||||||
|
entities = append(entities, iden.Email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ictx = clues.Add(ictx, "link_share_entity_ids", strings.Join(entities, ","))
|
ictx = clues.Add(ictx, "link_share_entities", clues.HideAll(entities))
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/graph/api/driveitem-createlink?view=graph-rest-beta&tabs=http
|
// https://learn.microsoft.com/en-us/graph/api/driveitem-createlink?view=graph-rest-beta&tabs=http
|
||||||
// v1.0 version of the graph API does not support creating a
|
// v1.0 version of the graph API does not support creating a
|
||||||
@ -471,11 +477,14 @@ func filterUnavailableEntitiesInLinkShare(
|
|||||||
|
|
||||||
switch e.EntityType {
|
switch e.EntityType {
|
||||||
case metadata.GV2User:
|
case metadata.GV2User:
|
||||||
_, ok := availableEntities.Users.NameOf(e.ID)
|
// Link shares with external users won't have IDs
|
||||||
available = available || ok
|
if len(e.ID) == 0 && len(e.Email) > 0 {
|
||||||
|
available = true
|
||||||
|
} else {
|
||||||
|
_, available = availableEntities.Users.NameOf(e.ID)
|
||||||
|
}
|
||||||
case metadata.GV2Group:
|
case metadata.GV2Group:
|
||||||
_, ok := availableEntities.Groups.NameOf(e.ID)
|
_, available = availableEntities.Groups.NameOf(e.ID)
|
||||||
available = available || ok
|
|
||||||
default:
|
default:
|
||||||
// We only know about users and groups
|
// We only know about users and groups
|
||||||
available = true
|
available = true
|
||||||
|
|||||||
@ -731,10 +731,24 @@ func linkSharesEqual(expected metadata.LinkShare, got metadata.LinkShare) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Equal(expected.Entities, got.Entities) {
|
if len(expected.Entities) != len(got.Entities) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, e := range expected.Entities {
|
||||||
|
if !strings.EqualFold(e.ID, got.Entities[i].ID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(e.Email, got.Entities[i].Email) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.EntityType != got.Entities[i].EntityType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (expected.Expiration == nil && got.Expiration != nil) ||
|
if (expected.Expiration == nil && got.Expiration != nil) ||
|
||||||
(expected.Expiration != nil && got.Expiration == nil) {
|
(expected.Expiration != nil && got.Expiration == nil) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -1083,8 +1083,19 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
_, secondaryUserID := suite.SecondaryUser()
|
secondaryUserName, secondaryUserID := suite.SecondaryUser()
|
||||||
_, tertiaryUserID := suite.TertiaryUser()
|
secondaryUser := metadata.Entity{
|
||||||
|
ID: secondaryUserID,
|
||||||
|
Email: secondaryUserName,
|
||||||
|
EntityType: metadata.GV2User,
|
||||||
|
}
|
||||||
|
|
||||||
|
tertiaryUserName, tertiaryUserID := suite.TertiaryUser()
|
||||||
|
tertiaryUser := metadata.Entity{
|
||||||
|
ID: tertiaryUserID,
|
||||||
|
Email: tertiaryUserName,
|
||||||
|
EntityType: metadata.GV2User,
|
||||||
|
}
|
||||||
|
|
||||||
// Get the default drive ID for the test user.
|
// Get the default drive ID for the test user.
|
||||||
driveID := mustGetDefaultDriveID(
|
driveID := mustGetDefaultDriveID(
|
||||||
@ -1138,9 +1149,9 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
|
|||||||
Meta: stub.MetaData{
|
Meta: stub.MetaData{
|
||||||
LinkShares: []stub.LinkShareData{
|
LinkShares: []stub.LinkShareData{
|
||||||
{
|
{
|
||||||
EntityIDs: []string{secondaryUserID},
|
Entities: []metadata.Entity{secondaryUser},
|
||||||
Scope: "users",
|
Scope: "users",
|
||||||
Type: "edit",
|
Type: "edit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SharingMode: metadata.SharingModeCustom,
|
SharingMode: metadata.SharingModeCustom,
|
||||||
@ -1199,9 +1210,9 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
|
|||||||
Meta: stub.MetaData{
|
Meta: stub.MetaData{
|
||||||
LinkShares: []stub.LinkShareData{
|
LinkShares: []stub.LinkShareData{
|
||||||
{
|
{
|
||||||
EntityIDs: []string{tertiaryUserID},
|
Entities: []metadata.Entity{tertiaryUser},
|
||||||
Scope: "anonymous",
|
Scope: "anonymous",
|
||||||
Type: "edit",
|
Type: "edit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1212,9 +1223,9 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
|
|||||||
Meta: stub.MetaData{
|
Meta: stub.MetaData{
|
||||||
LinkShares: []stub.LinkShareData{
|
LinkShares: []stub.LinkShareData{
|
||||||
{
|
{
|
||||||
EntityIDs: []string{tertiaryUserID},
|
Entities: []metadata.Entity{tertiaryUser},
|
||||||
Scope: "users",
|
Scope: "users",
|
||||||
Type: "edit",
|
Type: "edit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SharingMode: metadata.SharingModeCustom,
|
SharingMode: metadata.SharingModeCustom,
|
||||||
|
|||||||
@ -37,10 +37,9 @@ func getMetadata(fileName string, meta MetaData, permUseID bool) metadata.Metada
|
|||||||
id := uuid.NewString()
|
id := uuid.NewString()
|
||||||
uperm := metadata.Permission{ID: id, Roles: meta.Perms.Roles}
|
uperm := metadata.Permission{ID: id, Roles: meta.Perms.Roles}
|
||||||
|
|
||||||
|
uperm.Email = meta.Perms.User
|
||||||
if permUseID {
|
if permUseID {
|
||||||
uperm.EntityID = meta.Perms.EntityID
|
uperm.EntityID = meta.Perms.EntityID
|
||||||
} else {
|
|
||||||
uperm.Email = meta.Perms.User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testMeta.Permissions = []metadata.Permission{uperm}
|
testMeta.Permissions = []metadata.Permission{uperm}
|
||||||
@ -48,11 +47,18 @@ func getMetadata(fileName string, meta MetaData, permUseID bool) metadata.Metada
|
|||||||
|
|
||||||
if len(meta.LinkShares) != 0 {
|
if len(meta.LinkShares) != 0 {
|
||||||
for _, ls := range meta.LinkShares {
|
for _, ls := range meta.LinkShares {
|
||||||
id := strings.Join(ls.EntityIDs, "-") + ls.Scope + ls.Type
|
eids := []string{}
|
||||||
|
for _, id := range ls.Entities {
|
||||||
|
eids = append(eids, id.ID)
|
||||||
|
}
|
||||||
|
|
||||||
entities := []metadata.Entity{}
|
id := strings.Join(eids, "-") + ls.Scope + ls.Type
|
||||||
for _, e := range ls.EntityIDs {
|
|
||||||
entities = append(entities, metadata.Entity{ID: e, EntityType: "user"})
|
roles := []string{}
|
||||||
|
if ls.Type == "edit" {
|
||||||
|
roles = []string{"write"}
|
||||||
|
} else if ls.Type == "view" {
|
||||||
|
roles = []string{"read"}
|
||||||
}
|
}
|
||||||
|
|
||||||
ls := metadata.LinkShare{
|
ls := metadata.LinkShare{
|
||||||
@ -62,7 +68,8 @@ func getMetadata(fileName string, meta MetaData, permUseID bool) metadata.Metada
|
|||||||
Type: ls.Type,
|
Type: ls.Type,
|
||||||
WebURL: id,
|
WebURL: id,
|
||||||
},
|
},
|
||||||
Entities: entities,
|
Entities: ls.Entities,
|
||||||
|
Roles: roles,
|
||||||
}
|
}
|
||||||
|
|
||||||
testMeta.LinkShares = append(testMeta.LinkShares, ls)
|
testMeta.LinkShares = append(testMeta.LinkShares, ls)
|
||||||
@ -79,9 +86,9 @@ type PermData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LinkShareData struct {
|
type LinkShareData struct {
|
||||||
EntityIDs []string
|
Entities []metadata.Entity
|
||||||
Scope string
|
Scope string
|
||||||
Type string
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetaData struct {
|
type MetaData struct {
|
||||||
|
|||||||
@ -43,3 +43,5 @@ Below is a list of known Corso issues and limitations:
|
|||||||
updated the locale since the backup was created.
|
updated the locale since the backup was created.
|
||||||
|
|
||||||
* In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder.
|
* In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder.
|
||||||
|
|
||||||
|
* External users with access through shared links won't receive these links as they're not sent via email during restore.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user