Backup and restore link share information (#3655)
As of now we are only backing up and restoring actual permissions. This adds support for backing up and restoring link shares as well. --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [x] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [x] 🌻 Feature - [ ] 🐛 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/3605 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [ ] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
8683fbc067
commit
9d801efa03
@ -8,10 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased] (beta)
|
||||
|
||||
### Added
|
||||
- Drive items backup and restore link shares
|
||||
|
||||
### Fixed
|
||||
- Return a ServiceNotEnabled error when a tenant has no active SharePoint license.
|
||||
|
||||
### Known issues
|
||||
- If a link share is created for an item with inheritance disabled
|
||||
(via the Graph API), the link shares restored in that item will
|
||||
not be inheritable by children
|
||||
- Link shares with password protection can't be restored
|
||||
|
||||
## [v0.10.0] (beta) - 2023-06-26
|
||||
|
||||
### Added
|
||||
|
||||
@ -274,12 +274,14 @@ func generateAndRestoreDriveItems(
|
||||
{
|
||||
Name: fmt.Sprintf("file-1st-count-%d-at-%s", i, currentTime),
|
||||
Data: fileAData,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: writePerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: fmt.Sprintf("file-2nd-count-%d-at-%s", i, currentTime),
|
||||
Data: fileBData,
|
||||
@ -291,14 +293,17 @@ func generateAndRestoreDriveItems(
|
||||
},
|
||||
{
|
||||
Name: folderAName,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: folderCName,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -307,6 +312,7 @@ func generateAndRestoreDriveItems(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// a folder that has permissions with an item in the folder with
|
||||
// the different permissions.
|
||||
@ -315,6 +321,7 @@ func generateAndRestoreDriveItems(
|
||||
{
|
||||
Name: fmt.Sprintf("file-count-%d-at-%s", i, currentTime),
|
||||
Data: fileEData,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -322,12 +329,15 @@ func generateAndRestoreDriveItems(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// a folder that has permissions with an item in the folder with
|
||||
// no permissions.
|
||||
@ -338,12 +348,14 @@ func generateAndRestoreDriveItems(
|
||||
Data: fileAData,
|
||||
},
|
||||
},
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: folderBPath,
|
||||
Files: []odStub.ItemData{
|
||||
@ -352,6 +364,7 @@ func generateAndRestoreDriveItems(
|
||||
// permissions.
|
||||
Name: fmt.Sprintf("file-count-%d-at-%s", i, currentTime),
|
||||
Data: fileBData,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -359,9 +372,11 @@ func generateAndRestoreDriveItems(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Folders: []odStub.ItemData{
|
||||
{
|
||||
Name: folderAName,
|
||||
Meta: odStub.MetaData{
|
||||
Perms: odStub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -370,6 +385,7 @@ func generateAndRestoreDriveItems(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cols = append(cols, col...)
|
||||
|
||||
@ -684,6 +684,33 @@ func permissionEqual(expected metadata.Permission, got metadata.Permission) bool
|
||||
return true
|
||||
}
|
||||
|
||||
func linkSharesEqual(expected metadata.LinkShare, got metadata.LinkShare) bool {
|
||||
if !strings.EqualFold(expected.Link.Scope, got.Link.Scope) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.EqualFold(expected.Link.Type, got.Link.Type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !slices.Equal(expected.Entities, got.Entities) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (expected.Expiration == nil && got.Expiration != nil) ||
|
||||
(expected.Expiration != nil && got.Expiration == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
if expected.Expiration != nil &&
|
||||
got.Expiration != nil &&
|
||||
!expected.Expiration.Equal(ptr.Val(got.Expiration)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func compareDriveItem(
|
||||
t *testing.T,
|
||||
expected map[string][]byte,
|
||||
@ -773,6 +800,8 @@ func compareDriveItem(
|
||||
return true
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedMeta.SharingMode, itemMeta.SharingMode, "sharing mode")
|
||||
|
||||
// We cannot restore owner permissions, so skip checking them
|
||||
itemPerms := []metadata.Permission{}
|
||||
|
||||
@ -792,6 +821,13 @@ func compareDriveItem(
|
||||
config.Service == path.SharePointService,
|
||||
permissionEqual)
|
||||
|
||||
testElementsMatch(
|
||||
t,
|
||||
expectedMeta.LinkShares,
|
||||
itemMeta.LinkShares,
|
||||
false,
|
||||
linkSharesEqual)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -80,13 +80,14 @@ type GetItemer interface {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type RestoreHandler interface {
|
||||
DeleteItemPermissioner
|
||||
GetFolderByNamer
|
||||
GetRootFolderer
|
||||
ItemInfoAugmenter
|
||||
NewItemContentUploader
|
||||
PostItemInContainerer
|
||||
DeleteItemPermissioner
|
||||
UpdateItemPermissioner
|
||||
UpdateItemLinkSharer
|
||||
}
|
||||
|
||||
type NewItemContentUploader interface {
|
||||
@ -113,6 +114,14 @@ type UpdateItemPermissioner interface {
|
||||
) (drives.ItemItemsItemInviteResponseable, error)
|
||||
}
|
||||
|
||||
type UpdateItemLinkSharer interface {
|
||||
PostItemLinkShareUpdate(
|
||||
ctx context.Context,
|
||||
driveID, itemID string,
|
||||
body *drives.ItemItemsItemCreateLinkPostRequestBody,
|
||||
) (models.Permissionable, error)
|
||||
}
|
||||
|
||||
type PostItemInContainerer interface {
|
||||
PostItemInContainer(
|
||||
ctx context.Context,
|
||||
|
||||
@ -109,6 +109,7 @@ func downloadItemMeta(
|
||||
}
|
||||
|
||||
meta.Permissions = metadata.FilterPermissions(ctx, perm.GetValue())
|
||||
meta.LinkShares = metadata.FilterLinkShares(ctx, perm.GetValue())
|
||||
}
|
||||
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
|
||||
@ -169,6 +169,14 @@ func (h itemRestoreHandler) PostItemPermissionUpdate(
|
||||
return h.ac.PostItemPermissionUpdate(ctx, driveID, itemID, body)
|
||||
}
|
||||
|
||||
func (h itemRestoreHandler) PostItemLinkShareUpdate(
|
||||
ctx context.Context,
|
||||
driveID, itemID string,
|
||||
body *drives.ItemItemsItemCreateLinkPostRequestBody,
|
||||
) (models.Permissionable, error) {
|
||||
return h.ac.PostItemLinkShareUpdate(ctx, driveID, itemID, body)
|
||||
}
|
||||
|
||||
func (h itemRestoreHandler) PostItemInContainer(
|
||||
ctx context.Context,
|
||||
driveID, parentFolderID string,
|
||||
|
||||
@ -5,6 +5,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Entity struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
EntityType GV2Type `json:"entityType,omitempty"`
|
||||
}
|
||||
|
||||
type LinkShareLink struct {
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
WebURL string `json:"webUrl,omitempty"` // we cannot restore this, but can be used for comparisons
|
||||
PreventsDownload bool `json:"preventsDownload,omitempty"`
|
||||
}
|
||||
|
||||
type LinkShare struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Link LinkShareLink `json:"link,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
Entities []Entity `json:"entities,omitempty"` // this is the resource owner's ID
|
||||
HasPassword bool `json:"hasPassword,omitempty"` // We cannot restore ones with password
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
}
|
||||
|
||||
func (ls LinkShare) Equals(other LinkShare) bool {
|
||||
return ls.Link.WebURL == other.Link.WebURL
|
||||
}
|
||||
|
||||
// ItemMeta contains metadata about the Item. It gets stored in a
|
||||
// separate file in kopia
|
||||
type Metadata struct {
|
||||
@ -14,6 +39,7 @@ type Metadata struct {
|
||||
// - custom: use Permissions to set correct permissions ("shared" has value in delta)
|
||||
SharingMode SharingMode `json:"permissionMode,omitempty"`
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
LinkShares []LinkShare `json:"linkShares,omitempty"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
|
||||
@ -40,13 +40,14 @@ type Permission struct {
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
}
|
||||
|
||||
// isSamePermission checks equality of two UserPermission objects
|
||||
// Equal checks equality of two UserPermission objects
|
||||
func (p Permission) Equals(other Permission) bool {
|
||||
// EntityID can be empty for older backups and Email can be empty
|
||||
// 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 p.EntityID != "" && p.EntityID != other.EntityID {
|
||||
return false
|
||||
}
|
||||
@ -64,19 +65,51 @@ func (p Permission) Equals(other Permission) bool {
|
||||
return slices.Equal(p1r, p2r)
|
||||
}
|
||||
|
||||
// DiffLinkShares is just a wrapper on top of DiffPermissions but we
|
||||
// filter out link shares which do not have any associated users. This
|
||||
// is useful for two reason:
|
||||
// - When a user creates a link share on parent after creating a child
|
||||
// link with `retainInheritedPermissisons`, all the previous link shares
|
||||
// are inherited onto the child but without any users associated with
|
||||
// the share. We have to drop the empty ones to make sure we reset.
|
||||
// - We are restoring link shares so that we can restore permissions for
|
||||
// the user, but restoring links without users is not useful.
|
||||
func DiffLinkShares(current, expected []LinkShare) ([]LinkShare, []LinkShare) {
|
||||
filteredCurrent := []LinkShare{}
|
||||
filteredExpected := []LinkShare{}
|
||||
|
||||
for _, ls := range current {
|
||||
if len(ls.Entities) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredCurrent = append(filteredCurrent, ls)
|
||||
}
|
||||
|
||||
for _, ls := range expected {
|
||||
if len(ls.Entities) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredExpected = append(filteredExpected, ls)
|
||||
}
|
||||
|
||||
return DiffPermissions(filteredCurrent, filteredExpected)
|
||||
}
|
||||
|
||||
// DiffPermissions compares the before and after set, returning
|
||||
// the permissions that were added and removed (in that order)
|
||||
// in the after set.
|
||||
func DiffPermissions(before, after []Permission) ([]Permission, []Permission) {
|
||||
func DiffPermissions[T interface{ Equals(T) bool }](current, expected []T) ([]T, []T) {
|
||||
var (
|
||||
added = []Permission{}
|
||||
removed = []Permission{}
|
||||
added = []T{}
|
||||
removed = []T{}
|
||||
)
|
||||
|
||||
for _, cp := range after {
|
||||
for _, cp := range expected {
|
||||
found := false
|
||||
|
||||
for _, pp := range before {
|
||||
for _, pp := range current {
|
||||
if cp.Equals(pp) {
|
||||
found = true
|
||||
break
|
||||
@ -88,10 +121,10 @@ func DiffPermissions(before, after []Permission) ([]Permission, []Permission) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, pp := range before {
|
||||
for _, pp := range current {
|
||||
found := false
|
||||
|
||||
for _, cp := range after {
|
||||
for _, cp := range expected {
|
||||
if cp.Equals(pp) {
|
||||
found = true
|
||||
break
|
||||
@ -116,7 +149,6 @@ func FilterPermissions(ctx context.Context, perms []models.Permissionable) []Per
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
// Below are the mapping from roles to "Advanced" permissions
|
||||
// screen entries:
|
||||
//
|
||||
@ -127,10 +159,81 @@ func FilterPermissions(ctx context.Context, perms []models.Permissionable) []Per
|
||||
//
|
||||
// helpful docs:
|
||||
// https://devblogs.microsoft.com/microsoft365dev/controlling-app-access-on-specific-sharepoint-site-collections/
|
||||
roles := p.GetRoles()
|
||||
|
||||
gv2t, entityID := getIdentityDetails(ctx, p.GetGrantedToV2())
|
||||
|
||||
// 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, Permission{
|
||||
ID: ptr.Val(p.GetId()),
|
||||
Roles: roles,
|
||||
EntityID: entityID,
|
||||
EntityType: gv2t,
|
||||
Expiration: p.GetExpirationDateTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return up
|
||||
}
|
||||
|
||||
func FilterLinkShares(ctx context.Context, perms []models.Permissionable) []LinkShare {
|
||||
up := []LinkShare{}
|
||||
|
||||
for _, p := range perms {
|
||||
link := p.GetLink()
|
||||
if link == nil {
|
||||
// Non link share based permissions are handled separately
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
roles = p.GetRoles()
|
||||
gv2 = p.GetGrantedToV2()
|
||||
entityID string
|
||||
gv2 = p.GetGrantedToIdentitiesV2()
|
||||
)
|
||||
|
||||
idens := []Entity{}
|
||||
|
||||
for _, g := range gv2 {
|
||||
gv2t, entityID := getIdentityDetails(ctx, g)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
idens = append(idens, Entity{ID: entityID, EntityType: gv2t})
|
||||
}
|
||||
|
||||
up = append(up, LinkShare{
|
||||
ID: ptr.Val(p.GetId()),
|
||||
Link: LinkShareLink{
|
||||
Scope: ptr.Val(link.GetScope()),
|
||||
Type: ptr.Val(link.GetType()),
|
||||
WebURL: ptr.Val(link.GetWebUrl()),
|
||||
PreventsDownload: ptr.Val(link.GetPreventsDownload()),
|
||||
},
|
||||
Roles: roles,
|
||||
Entities: idens,
|
||||
HasPassword: ptr.Val(p.GetHasPassword()),
|
||||
Expiration: p.GetExpirationDateTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return up
|
||||
}
|
||||
|
||||
func getIdentityDetails(ctx context.Context, gv2 models.SharePointIdentitySetable) (GV2Type, string) {
|
||||
var (
|
||||
gv2t GV2Type
|
||||
entityID string
|
||||
)
|
||||
|
||||
switch true {
|
||||
@ -156,21 +259,5 @@ func FilterPermissions(ctx context.Context, perms []models.Permissionable) []Per
|
||||
logger.Ctx(ctx).Info("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, Permission{
|
||||
ID: ptr.Val(p.GetId()),
|
||||
Roles: roles,
|
||||
EntityID: entityID,
|
||||
EntityType: gv2t,
|
||||
Expiration: p.GetExpirationDateTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return up
|
||||
return gv2t, entityID
|
||||
}
|
||||
|
||||
@ -149,6 +149,71 @@ func (suite *PermissionsUnitTestSuite) TestDiffPermissions() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PermissionsUnitTestSuite) TestDiffLinkShares() {
|
||||
entities1 := []Entity{{ID: "e1"}}
|
||||
ls1 := LinkShare{
|
||||
ID: "id1",
|
||||
Entities: entities1,
|
||||
Link: LinkShareLink{WebURL: "id1"},
|
||||
}
|
||||
|
||||
lsempty := LinkShare{
|
||||
ID: "id2",
|
||||
Link: LinkShareLink{WebURL: "id2"},
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
name string
|
||||
before []LinkShare
|
||||
after []LinkShare
|
||||
added []LinkShare
|
||||
removed []LinkShare
|
||||
}{
|
||||
{
|
||||
name: "single link share added",
|
||||
before: []LinkShare{},
|
||||
after: []LinkShare{ls1},
|
||||
added: []LinkShare{ls1},
|
||||
removed: []LinkShare{},
|
||||
},
|
||||
{
|
||||
name: "empty filtered from before",
|
||||
before: []LinkShare{lsempty},
|
||||
after: []LinkShare{},
|
||||
added: []LinkShare{},
|
||||
removed: []LinkShare{},
|
||||
},
|
||||
{
|
||||
name: "empty filtered from after",
|
||||
before: []LinkShare{},
|
||||
after: []LinkShare{lsempty},
|
||||
added: []LinkShare{},
|
||||
removed: []LinkShare{},
|
||||
},
|
||||
{
|
||||
name: "empty filtered from both",
|
||||
before: []LinkShare{lsempty, ls1},
|
||||
after: []LinkShare{lsempty},
|
||||
added: []LinkShare{},
|
||||
removed: []LinkShare{ls1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
_, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
added, removed := DiffLinkShares(test.before, test.after)
|
||||
|
||||
assert.Equal(t, added, test.added, "added link shares")
|
||||
assert.Equal(t, removed, test.removed, "removed link shares")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getPermsAndResourceOwnerPerms(
|
||||
permID, resourceOwner string,
|
||||
gv2t GV2Type,
|
||||
|
||||
@ -268,6 +268,14 @@ func (h RestoreHandler) PostItemPermissionUpdate(
|
||||
return nil, clues.New("not implemented")
|
||||
}
|
||||
|
||||
func (h RestoreHandler) PostItemLinkShareUpdate(
|
||||
ctx context.Context,
|
||||
driveID, itemID string,
|
||||
body *drives.ItemItemsItemCreateLinkPostRequestBody,
|
||||
) (models.Permissionable, error) {
|
||||
return nil, clues.New("not implemented")
|
||||
}
|
||||
|
||||
func (h RestoreHandler) PostItemInContainer(
|
||||
context.Context,
|
||||
string, string,
|
||||
|
||||
@ -2,6 +2,7 @@ package onedrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
||||
@ -11,6 +12,7 @@ import (
|
||||
"github.com/alcionai/corso/src/internal/data"
|
||||
"github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
|
||||
"github.com/alcionai/corso/src/internal/version"
|
||||
"github.com/alcionai/corso/src/pkg/logger"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
)
|
||||
|
||||
@ -81,11 +83,58 @@ func getCollectionMetadata(
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// computeParentPermissions computes the parent permissions by
|
||||
// Unlike permissions, link shares are inherited from all the parents
|
||||
// of an item and not just the direct parent.
|
||||
func computePreviousLinkShares(
|
||||
ctx context.Context,
|
||||
originDir path.Path,
|
||||
parentMetas map[string]metadata.Metadata,
|
||||
) ([]metadata.LinkShare, error) {
|
||||
linkShares := []metadata.LinkShare{}
|
||||
ctx = clues.Add(ctx, "origin_dir", originDir)
|
||||
|
||||
parent, err := originDir.Dir()
|
||||
if err != nil {
|
||||
return nil, clues.New("getting parent").WithClues(ctx)
|
||||
}
|
||||
|
||||
for len(parent.Elements()) > 0 {
|
||||
ictx := clues.Add(ctx, "current_ancestor_dir", parent)
|
||||
|
||||
drivePath, err := path.ToDrivePath(parent)
|
||||
if err != nil {
|
||||
return nil, clues.New("transforming dir to drivePath").WithClues(ictx)
|
||||
}
|
||||
|
||||
if len(drivePath.Folders) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
meta, ok := parentMetas[parent.String()]
|
||||
if !ok {
|
||||
return nil, clues.New("no metadata found in parent").WithClues(ictx)
|
||||
}
|
||||
|
||||
// Any change in permissions would change it to custom
|
||||
// permission set and so we can filter on that.
|
||||
if meta.SharingMode == metadata.SharingModeCustom {
|
||||
linkShares = append(linkShares, meta.LinkShares...)
|
||||
}
|
||||
|
||||
parent, err = parent.Dir()
|
||||
if err != nil {
|
||||
return nil, clues.New("getting parent").WithClues(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return linkShares, nil
|
||||
}
|
||||
|
||||
// computePreviousMetadata computes the parent permissions by
|
||||
// traversing parentMetas and finding the first item with custom
|
||||
// permissions. parentMetas is expected to have all the parent
|
||||
// directory metas for this to work.
|
||||
func computeParentPermissions(
|
||||
func computePreviousMetadata(
|
||||
ctx context.Context,
|
||||
originDir path.Path,
|
||||
// map parent dir -> parent's metadata
|
||||
@ -227,6 +276,128 @@ func UpdatePermissions(
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateDeleteItemLinkSharer interface {
|
||||
DeleteItemPermissioner // Deletion logic is same as permissions
|
||||
UpdateItemLinkSharer
|
||||
}
|
||||
|
||||
func UpdateLinkShares(
|
||||
ctx context.Context,
|
||||
upils updateDeleteItemLinkSharer,
|
||||
driveID string,
|
||||
itemID string,
|
||||
lsAdded, lsRemoved []metadata.LinkShare,
|
||||
oldLinkShareIDToNewID map[string]string,
|
||||
) (bool, error) {
|
||||
// You can only delete inherited sharing links the first time you
|
||||
// create a sharing link which is done using
|
||||
// `retainInheritedPermissions`. We cannot separately delete any
|
||||
// inherited link shares via DELETE API call like for permissions.
|
||||
alreadyDeleted := false
|
||||
|
||||
for _, ls := range lsAdded {
|
||||
ictx := clues.Add(ctx, "link_share_id", ls.ID)
|
||||
|
||||
// Links with password are not shared with a specific user
|
||||
// even when we select a particular user, plus we are not
|
||||
// able to get the password or retain the original link and
|
||||
// so restoring them makes no sense.
|
||||
if ls.HasPassword {
|
||||
continue
|
||||
}
|
||||
|
||||
idens := []map[string]string{}
|
||||
entities := []string{}
|
||||
|
||||
for _, iden := range ls.Entities {
|
||||
// TODO: sitegroup support. Currently errors with "One or more users could not be resolved",
|
||||
// likely due to the site group entityID consisting of a single integer (ex: 4)
|
||||
if iden.EntityType == metadata.GV2SiteGroup {
|
||||
continue
|
||||
}
|
||||
|
||||
// Using DriveRecipient seems to error out on Graph end
|
||||
idens = append(idens, map[string]string{"objectId": iden.ID})
|
||||
entities = append(entities, iden.ID)
|
||||
}
|
||||
|
||||
ictx = clues.Add(ictx, "link_share_entity_ids", strings.Join(entities, ","))
|
||||
|
||||
// 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
|
||||
// link without sending a notification to the user and so we
|
||||
// use the beta API. Since we use the v1.0 API, we have to
|
||||
// stuff some of the data into the AdditionalData fields as
|
||||
// the actual fields don't exist in the stable sdk.
|
||||
// Here is the data that we have to send:
|
||||
// {
|
||||
// "type": "view",
|
||||
// "scope": "anonymous",
|
||||
// "password": "String",
|
||||
// "expirationDateTime": "...",
|
||||
// "recipients": [{"@odata.type": "microsoft.graph.driveRecipient"}],
|
||||
// "sendNotification": true,
|
||||
// "retainInheritedPermissions": false
|
||||
// }
|
||||
lsbody := drives.NewItemItemsItemCreateLinkPostRequestBody()
|
||||
lsbody.SetType(ptr.To(ls.Link.Type))
|
||||
lsbody.SetScope(ptr.To(ls.Link.Scope))
|
||||
lsbody.SetExpirationDateTime(ls.Expiration)
|
||||
|
||||
ad := map[string]any{
|
||||
"sendNotification": false,
|
||||
"recipients": idens,
|
||||
}
|
||||
lsbody.SetAdditionalData(ad)
|
||||
|
||||
if !alreadyDeleted {
|
||||
// The only way to delete any is to use this and so if
|
||||
// we have any deleted items, we can be sure that all the
|
||||
// inherited permissions would have been removed.
|
||||
lsbody.SetRetainInheritedPermissions(ptr.To(len(lsRemoved) == 0))
|
||||
|
||||
// This value only effective on the first call, but lets
|
||||
// make sure to not send it on followups.
|
||||
alreadyDeleted = true
|
||||
}
|
||||
|
||||
newLS, err := upils.PostItemLinkShareUpdate(ictx, driveID, itemID, lsbody)
|
||||
if err != nil {
|
||||
return alreadyDeleted, clues.Stack(err)
|
||||
}
|
||||
|
||||
oldLinkShareIDToNewID[ls.ID] = ptr.Val(newLS.GetId())
|
||||
}
|
||||
|
||||
// It is possible to have empty link shares even though we should
|
||||
// have inherited one if the user creates a link using
|
||||
// `retainInheritedPermissions` as false, but then deleted it. We
|
||||
// can recreate this by creating a link with no users and deleting it.
|
||||
if len(lsRemoved) > 0 && len(lsAdded) == 0 {
|
||||
lsbody := drives.NewItemItemsItemCreateLinkPostRequestBody()
|
||||
lsbody.SetType(ptr.To("view"))
|
||||
// creating a `users` link without any users ensure that even
|
||||
// if we fail to delete the link there are no links lying
|
||||
// around that could be used to access this
|
||||
lsbody.SetScope(ptr.To("users"))
|
||||
lsbody.SetRetainInheritedPermissions(ptr.To(false))
|
||||
|
||||
newLS, err := upils.PostItemLinkShareUpdate(ctx, driveID, itemID, lsbody)
|
||||
if err != nil {
|
||||
return alreadyDeleted, clues.Stack(err)
|
||||
}
|
||||
|
||||
alreadyDeleted = true
|
||||
|
||||
err = upils.DeleteItemPermission(ctx, driveID, itemID, ptr.Val(newLS.GetId()))
|
||||
if err != nil {
|
||||
return alreadyDeleted, clues.Stack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return alreadyDeleted, nil
|
||||
}
|
||||
|
||||
// RestorePermissions takes in the permissions of an item, computes
|
||||
// what permissions need to added and removed based on the parent
|
||||
// folder metas and uses that to add/remove the necessary permissions
|
||||
@ -246,14 +417,46 @@ func RestorePermissions(
|
||||
|
||||
ctx = clues.Add(ctx, "permission_item_id", itemID)
|
||||
|
||||
parents, err := computeParentPermissions(ctx, itemPath, caches.ParentDirToMeta)
|
||||
previousLinkShares, err := computePreviousLinkShares(ctx, itemPath, caches.ParentDirToMeta)
|
||||
if err != nil {
|
||||
return clues.Wrap(err, "parent permissions")
|
||||
return clues.Wrap(err, "previous link shares")
|
||||
}
|
||||
|
||||
permAdded, permRemoved := metadata.DiffPermissions(parents.Permissions, current.Permissions)
|
||||
lsAdded, lsRemoved := metadata.DiffLinkShares(previousLinkShares, current.LinkShares)
|
||||
|
||||
return UpdatePermissions(
|
||||
// Link shares have to be updated before permissions as we have to
|
||||
// use the information about if we had to reset the inheritance to
|
||||
// decide if we have to restore all the permissions.
|
||||
didReset, err := UpdateLinkShares(
|
||||
ctx,
|
||||
rh,
|
||||
driveID,
|
||||
itemID,
|
||||
lsAdded,
|
||||
lsRemoved,
|
||||
caches.OldLinkShareIDToNewID)
|
||||
if err != nil {
|
||||
return clues.Wrap(err, "updating link shares")
|
||||
}
|
||||
|
||||
previous, err := computePreviousMetadata(ctx, itemPath, caches.ParentDirToMeta)
|
||||
if err != nil {
|
||||
return clues.Wrap(err, "previous metadata")
|
||||
}
|
||||
|
||||
permAdded, permRemoved := metadata.DiffPermissions(previous.Permissions, current.Permissions)
|
||||
|
||||
if didReset {
|
||||
// In case we did a reset of permissions when restoring link
|
||||
// shares, we have to make sure to restore all the permissions
|
||||
// that an item has as they too will be removed.
|
||||
logger.Ctx(ctx).Debug("link share creation reset all inherited permissions")
|
||||
|
||||
permRemoved = []metadata.Permission{}
|
||||
permAdded = current.Permissions
|
||||
}
|
||||
|
||||
err = UpdatePermissions(
|
||||
ctx,
|
||||
rh,
|
||||
driveID,
|
||||
@ -261,4 +464,9 @@ func RestorePermissions(
|
||||
permAdded,
|
||||
permRemoved,
|
||||
caches.OldPermIDToNewID)
|
||||
if err != nil {
|
||||
return clues.Wrap(err, "updating permissions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -156,7 +156,7 @@ func runComputeParentPermissionsTest(
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
m, err := computeParentPermissions(ctx, test.item, test.parentPerms)
|
||||
m, err := computePreviousMetadata(ctx, test.item, test.parentPerms)
|
||||
require.NoError(t, err, "compute permissions")
|
||||
|
||||
assert.Equal(t, m, test.meta)
|
||||
|
||||
@ -39,6 +39,7 @@ type restoreCaches struct {
|
||||
Folders *folderCache
|
||||
ParentDirToMeta map[string]metadata.Metadata
|
||||
OldPermIDToNewID map[string]string
|
||||
OldLinkShareIDToNewID map[string]string
|
||||
DriveIDToRootFolderID map[string]string
|
||||
pool sync.Pool
|
||||
}
|
||||
@ -48,6 +49,7 @@ func NewRestoreCaches() *restoreCaches {
|
||||
Folders: NewFolderCache(),
|
||||
ParentDirToMeta: map[string]metadata.Metadata{},
|
||||
OldPermIDToNewID: map[string]string{},
|
||||
OldLinkShareIDToNewID: map[string]string{},
|
||||
DriveIDToRootFolderID: map[string]string{},
|
||||
// Buffer pool for uploads
|
||||
pool: sync.Pool{
|
||||
|
||||
@ -3,6 +3,7 @@ package stub
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/google/uuid"
|
||||
@ -18,30 +19,53 @@ import (
|
||||
// permission instead of email
|
||||
const versionPermissionSwitchedToID = version.OneDrive4DirIncludesPermissions
|
||||
|
||||
func getMetadata(fileName string, perm PermData, permUseID bool) metadata.Metadata {
|
||||
if len(perm.User) == 0 || len(perm.Roles) == 0 ||
|
||||
perm.SharingMode != metadata.SharingModeCustom {
|
||||
func getMetadata(fileName string, meta MetaData, permUseID bool) metadata.Metadata {
|
||||
if meta.SharingMode != metadata.SharingModeCustom {
|
||||
return metadata.Metadata{
|
||||
FileName: fileName,
|
||||
SharingMode: perm.SharingMode,
|
||||
SharingMode: meta.SharingMode,
|
||||
}
|
||||
}
|
||||
|
||||
testMeta := metadata.Metadata{FileName: fileName}
|
||||
|
||||
if len(meta.Perms.User) != 0 {
|
||||
// In case of permissions, the id will usually be same for same
|
||||
// user/role combo unless deleted and readded, but we have to do
|
||||
// this as we only have two users of which one is already taken.
|
||||
id := uuid.NewString()
|
||||
uperm := metadata.Permission{ID: id, Roles: perm.Roles}
|
||||
uperm := metadata.Permission{ID: id, Roles: meta.Perms.Roles}
|
||||
|
||||
if permUseID {
|
||||
uperm.EntityID = perm.EntityID
|
||||
uperm.EntityID = meta.Perms.EntityID
|
||||
} else {
|
||||
uperm.Email = perm.User
|
||||
uperm.Email = meta.Perms.User
|
||||
}
|
||||
|
||||
testMeta := metadata.Metadata{
|
||||
FileName: fileName,
|
||||
Permissions: []metadata.Permission{uperm},
|
||||
testMeta.Permissions = []metadata.Permission{uperm}
|
||||
}
|
||||
|
||||
if len(meta.LinkShares) != 0 {
|
||||
for _, ls := range meta.LinkShares {
|
||||
id := strings.Join(ls.EntityIDs, "-") + ls.Scope + ls.Type
|
||||
|
||||
entities := []metadata.Entity{}
|
||||
for _, e := range ls.EntityIDs {
|
||||
entities = append(entities, metadata.Entity{ID: e, EntityType: "user"})
|
||||
}
|
||||
|
||||
ls := metadata.LinkShare{
|
||||
ID: id, // id is required for mapping from parent
|
||||
Link: metadata.LinkShareLink{
|
||||
Scope: ls.Scope,
|
||||
Type: ls.Type,
|
||||
WebURL: id,
|
||||
},
|
||||
Entities: entities,
|
||||
}
|
||||
|
||||
testMeta.LinkShares = append(testMeta.LinkShares, ls)
|
||||
}
|
||||
}
|
||||
|
||||
return testMeta
|
||||
@ -51,20 +75,31 @@ type PermData struct {
|
||||
User string // user is only for older versions
|
||||
EntityID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
type LinkShareData struct {
|
||||
EntityIDs []string
|
||||
Scope string
|
||||
Type string
|
||||
}
|
||||
|
||||
type MetaData struct {
|
||||
SharingMode metadata.SharingMode
|
||||
Perms PermData
|
||||
LinkShares []LinkShareData
|
||||
}
|
||||
|
||||
type ItemData struct {
|
||||
Name string
|
||||
Data []byte
|
||||
Perms PermData
|
||||
Meta MetaData
|
||||
}
|
||||
|
||||
type ColInfo struct {
|
||||
PathElements []string
|
||||
Perms PermData
|
||||
Files []ItemData
|
||||
Folders []ItemData
|
||||
Meta MetaData
|
||||
}
|
||||
|
||||
type collection struct {
|
||||
@ -115,20 +150,20 @@ func DataForInfo(
|
||||
onedriveCol := NewCollection(service, c.PathElements, backupVersion)
|
||||
|
||||
for _, f := range c.Files {
|
||||
_, err = onedriveCol.withFile(f.Name, f.Data, f.Perms)
|
||||
_, err = onedriveCol.withFile(f.Name, f.Data, f.Meta)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range c.Folders {
|
||||
_, err = onedriveCol.withFolder(d.Name, d.Perms)
|
||||
_, err = onedriveCol.withFolder(d.Name, d.Meta)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = onedriveCol.withPermissions(c.Perms)
|
||||
_, err = onedriveCol.withPermissions(c.Meta)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@ -139,7 +174,7 @@ func DataForInfo(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *collection) withFile(name string, fileData []byte, perm PermData) (*collection, error) {
|
||||
func (c *collection) withFile(name string, fileData []byte, meta MetaData) (*collection, error) {
|
||||
switch c.BackupVersion {
|
||||
case 0:
|
||||
// Lookups will occur using the most recent version of things so we need
|
||||
@ -171,7 +206,7 @@ func (c *collection) withFile(name string, fileData []byte, perm PermData) (*col
|
||||
"",
|
||||
name+metadata.MetaFileSuffix,
|
||||
name+metadata.MetaFileSuffix,
|
||||
perm,
|
||||
meta,
|
||||
c.BackupVersion >= versionPermissionSwitchedToID)
|
||||
if err != nil {
|
||||
return c, err
|
||||
@ -196,7 +231,7 @@ func (c *collection) withFile(name string, fileData []byte, perm PermData) (*col
|
||||
name,
|
||||
name+metadata.MetaFileSuffix,
|
||||
name,
|
||||
perm,
|
||||
meta,
|
||||
c.BackupVersion >= versionPermissionSwitchedToID)
|
||||
if err != nil {
|
||||
return c, err
|
||||
@ -212,7 +247,7 @@ func (c *collection) withFile(name string, fileData []byte, perm PermData) (*col
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *collection) withFolder(name string, perm PermData) (*collection, error) {
|
||||
func (c *collection) withFolder(name string, meta MetaData) (*collection, error) {
|
||||
switch c.BackupVersion {
|
||||
case 0, version.OneDrive4DirIncludesPermissions, version.OneDrive5DirMetaNoName,
|
||||
version.OneDrive6NameInMeta, version.OneDrive7LocationRef, version.All8MigrateUserPNToID:
|
||||
@ -223,7 +258,7 @@ func (c *collection) withFolder(name string, perm PermData) (*collection, error)
|
||||
"",
|
||||
name+metadata.DirMetaFileSuffix,
|
||||
name+metadata.DirMetaFileSuffix,
|
||||
perm,
|
||||
meta,
|
||||
c.BackupVersion >= versionPermissionSwitchedToID)
|
||||
|
||||
c.Items = append(c.Items, item)
|
||||
@ -241,7 +276,7 @@ func (c *collection) withFolder(name string, perm PermData) (*collection, error)
|
||||
|
||||
// withPermissions adds permissions to the folder represented by this
|
||||
// onedriveCollection.
|
||||
func (c *collection) withPermissions(perm PermData) (*collection, error) {
|
||||
func (c *collection) withPermissions(meta MetaData) (*collection, error) {
|
||||
// These versions didn't store permissions for the folder or didn't store them
|
||||
// in the folder's collection.
|
||||
if c.BackupVersion < version.OneDrive4DirIncludesPermissions {
|
||||
@ -264,7 +299,7 @@ func (c *collection) withPermissions(perm PermData) (*collection, error) {
|
||||
name,
|
||||
metaName+metadata.DirMetaFileSuffix,
|
||||
metaName+metadata.DirMetaFileSuffix,
|
||||
perm,
|
||||
meta,
|
||||
c.BackupVersion >= versionPermissionSwitchedToID)
|
||||
if err != nil {
|
||||
return c, err
|
||||
@ -304,10 +339,10 @@ func FileWithData(
|
||||
|
||||
func ItemWithMetadata(
|
||||
fileName, itemID, lookupKey string,
|
||||
perm PermData,
|
||||
meta MetaData,
|
||||
permUseID bool,
|
||||
) (m365Stub.ItemInfo, error) {
|
||||
testMeta := getMetadata(fileName, perm, permUseID)
|
||||
testMeta := getMetadata(fileName, meta, permUseID)
|
||||
|
||||
testMetaJSON, err := json.Marshal(testMeta)
|
||||
if err != nil {
|
||||
|
||||
@ -231,6 +231,11 @@ func (suite *SharePointIntegrationSuite) TestPermissionsInheritanceRestoreAndBac
|
||||
testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
|
||||
}
|
||||
|
||||
func (suite *SharePointIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() {
|
||||
suite.T().Skip("Temporarily disabled due to CI issues")
|
||||
testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
|
||||
}
|
||||
|
||||
func (suite *SharePointIntegrationSuite) TestRestoreFolderNamedFolderRegression() {
|
||||
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||
testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
|
||||
@ -291,6 +296,10 @@ func (suite *OneDriveIntegrationSuite) TestPermissionsInheritanceRestoreAndBacku
|
||||
testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
|
||||
}
|
||||
|
||||
func (suite *OneDriveIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() {
|
||||
testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
|
||||
}
|
||||
|
||||
func (suite *OneDriveIntegrationSuite) TestRestoreFolderNamedFolderRegression() {
|
||||
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||
testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
|
||||
@ -352,6 +361,10 @@ func (suite *OneDriveNightlySuite) TestPermissionsInheritanceRestoreAndBackup()
|
||||
testPermissionsInheritanceRestoreAndBackup(suite, version.OneDrive4DirIncludesPermissions)
|
||||
}
|
||||
|
||||
func (suite *OneDriveNightlySuite) TestLinkSharesInheritanceRestoreAndBackup() {
|
||||
testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
|
||||
}
|
||||
|
||||
func (suite *OneDriveNightlySuite) TestRestoreFolderNamedFolderRegression() {
|
||||
// No reason why it couldn't work with previous versions, but this is when it got introduced.
|
||||
testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
|
||||
@ -407,13 +420,17 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
||||
folderBName,
|
||||
}
|
||||
|
||||
defaultMetadata := stub.MetaData{SharingMode: metadata.SharingModeInherited}
|
||||
|
||||
cols := []stub.ColInfo{
|
||||
{
|
||||
PathElements: rootPath,
|
||||
Meta: defaultMetadata,
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileAData,
|
||||
Meta: defaultMetadata,
|
||||
},
|
||||
},
|
||||
Folders: []stub.ItemData{
|
||||
@ -427,10 +444,12 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
||||
},
|
||||
{
|
||||
PathElements: folderAPath,
|
||||
Meta: defaultMetadata,
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileBData,
|
||||
Meta: defaultMetadata,
|
||||
},
|
||||
},
|
||||
Folders: []stub.ItemData{
|
||||
@ -441,10 +460,12 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
||||
},
|
||||
{
|
||||
PathElements: subfolderBPath,
|
||||
Meta: defaultMetadata,
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileCData,
|
||||
Meta: defaultMetadata,
|
||||
},
|
||||
},
|
||||
Folders: []stub.ItemData{
|
||||
@ -455,19 +476,23 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
|
||||
},
|
||||
{
|
||||
PathElements: subfolderAPath,
|
||||
Meta: defaultMetadata,
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileDData,
|
||||
Meta: defaultMetadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: folderBPath,
|
||||
Meta: defaultMetadata,
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileEData,
|
||||
Meta: defaultMetadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -557,38 +582,52 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
cols := []stub.ColInfo{
|
||||
{
|
||||
PathElements: rootPath,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
// Test restoring a file that doesn't inherit permissions.
|
||||
Name: fileName,
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: writePerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test restoring a file that doesn't inherit permissions and has
|
||||
// no permissions.
|
||||
Name: fileName2,
|
||||
Data: fileBData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
},
|
||||
Folders: []stub.ItemData{
|
||||
{
|
||||
Name: folderBName,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: folderAName,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: folderCName,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -597,14 +636,19 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: folderBPath,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
Files: []stub.ItemData{
|
||||
{
|
||||
// Test restoring a file in a non-root folder that doesn't inherit
|
||||
// permissions.
|
||||
Name: fileName,
|
||||
Data: fileBData,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -612,9 +656,11 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Folders: []stub.ItemData{
|
||||
{
|
||||
Name: folderAName,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -623,6 +669,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: We can't currently support having custom permissions
|
||||
// with the same set of permissions internally
|
||||
// {
|
||||
@ -654,6 +701,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileEData,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
@ -661,12 +709,15 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Tests a folder that has permissions with an item in the folder with
|
||||
// no permissions.
|
||||
@ -675,14 +726,19 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
|
||||
@ -746,11 +802,14 @@ func testPermissionsBackupAndNoRestore(suite oneDriveSuite, startVersion int) {
|
||||
{
|
||||
Name: fileName,
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: writePerm,
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -860,32 +919,44 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
||||
folderCName,
|
||||
}
|
||||
|
||||
fileSet := []stub.ItemData{
|
||||
{
|
||||
fileCustom := stub.ItemData{
|
||||
Name: "file-custom",
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: secondaryUserName,
|
||||
EntityID: secondaryUserID,
|
||||
Roles: writePerm,
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "file-inherited",
|
||||
Data: fileAData,
|
||||
Perms: stub.PermData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "file-empty",
|
||||
Data: fileAData,
|
||||
Perms: stub.PermData{
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
}
|
||||
fileInherited := stub.ItemData{
|
||||
Name: "file-inherited",
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
}
|
||||
fileEmpty := stub.ItemData{
|
||||
Name: "file-empty",
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
}
|
||||
|
||||
// If parent is empty, then empty permissions would be inherited
|
||||
fileEmptyInherited := stub.ItemData{
|
||||
Name: "file-empty",
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
}
|
||||
|
||||
fileSet := []stub.ItemData{fileCustom, fileInherited, fileEmpty}
|
||||
fileSetEmpty := []stub.ItemData{fileCustom, fileInherited, fileEmptyInherited}
|
||||
|
||||
// Here is what this test is testing
|
||||
// - custom-permission-folder
|
||||
@ -912,6 +983,9 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
||||
Folders: []stub.ItemData{
|
||||
{Name: folderAName},
|
||||
},
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: folderAPath,
|
||||
@ -921,33 +995,231 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
|
||||
{Name: folderBName},
|
||||
{Name: folderCName},
|
||||
},
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: tertiaryUserName,
|
||||
EntityID: tertiaryUserID,
|
||||
Roles: readPerm,
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderAAPath,
|
||||
Files: fileSet,
|
||||
Meta: stub.MetaData{
|
||||
Perms: stub.PermData{
|
||||
User: tertiaryUserName,
|
||||
EntityID: tertiaryUserID,
|
||||
Roles: writePerm,
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderABPath,
|
||||
Files: fileSet,
|
||||
Perms: stub.PermData{
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderACPath,
|
||||
Files: fileSetEmpty,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
|
||||
require.NoError(suite.T(), err)
|
||||
bss := suite.Service().String()
|
||||
|
||||
for vn := startVersion; vn <= version.Backup; vn++ {
|
||||
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
|
||||
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, err := stub.DataForInfo(suite.Service(), cols, vn)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
testData := restoreBackupInfoMultiVersion{
|
||||
service: suite.Service(),
|
||||
resourceCat: suite.Resource(),
|
||||
backupVersion: vn,
|
||||
collectionsPrevious: input,
|
||||
collectionsLatest: expected,
|
||||
}
|
||||
|
||||
runRestoreBackupTestVersions(
|
||||
t,
|
||||
testData,
|
||||
suite.Tenant(),
|
||||
[]string{suite.ResourceOwner()},
|
||||
control.Options{
|
||||
RestorePermissions: true,
|
||||
ToggleFeatures: control.Toggles{},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion int) {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
_, secondaryUserID := suite.SecondaryUser()
|
||||
_, tertiaryUserID := suite.TertiaryUser()
|
||||
|
||||
// Get the default drive ID for the test user.
|
||||
driveID := mustGetDefaultDriveID(
|
||||
t,
|
||||
ctx,
|
||||
suite.APIClient(),
|
||||
suite.Service(),
|
||||
suite.ResourceOwner())
|
||||
|
||||
folderAName := "custom"
|
||||
folderBName := "inherited"
|
||||
folderCName := "empty"
|
||||
|
||||
rootPath := []string{
|
||||
odConsts.DrivesPathDir,
|
||||
driveID,
|
||||
odConsts.RootPathDir,
|
||||
}
|
||||
folderAPath := []string{
|
||||
odConsts.DrivesPathDir,
|
||||
driveID,
|
||||
odConsts.RootPathDir,
|
||||
folderAName,
|
||||
}
|
||||
subfolderAAPath := []string{
|
||||
odConsts.DrivesPathDir,
|
||||
driveID,
|
||||
odConsts.RootPathDir,
|
||||
folderAName,
|
||||
folderAName,
|
||||
}
|
||||
subfolderABPath := []string{
|
||||
odConsts.DrivesPathDir,
|
||||
driveID,
|
||||
odConsts.RootPathDir,
|
||||
folderAName,
|
||||
folderBName,
|
||||
}
|
||||
subfolderACPath := []string{
|
||||
odConsts.DrivesPathDir,
|
||||
driveID,
|
||||
odConsts.RootPathDir,
|
||||
folderAName,
|
||||
folderCName,
|
||||
}
|
||||
|
||||
fileSet := []stub.ItemData{
|
||||
{
|
||||
Name: "file-custom",
|
||||
Data: fileAData,
|
||||
Meta: stub.MetaData{
|
||||
LinkShares: []stub.LinkShareData{
|
||||
{
|
||||
EntityIDs: []string{secondaryUserID},
|
||||
Scope: "users",
|
||||
Type: "edit",
|
||||
},
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "file-inherited",
|
||||
Data: fileBData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "file-empty",
|
||||
Data: fileCData,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Here is what this test is testing
|
||||
// - custom-link-share-folder
|
||||
// - custom-link-share-file
|
||||
// - inherted-link-share-file
|
||||
// - empty-link-share-file
|
||||
// - custom-link-share-folder
|
||||
// - custom-link-share-file
|
||||
// - inherted-link-share-file
|
||||
// - empty-link-share-file
|
||||
// - inherted-link-share-folder
|
||||
// - custom-link-share-file
|
||||
// - inherted-link-share-file
|
||||
// - empty-link-share-file
|
||||
// - empty-link-share-folder
|
||||
// - custom-link-share-file
|
||||
// - inherted-link-share-file
|
||||
// - empty-link-share-file
|
||||
|
||||
cols := []stub.ColInfo{
|
||||
{
|
||||
PathElements: rootPath,
|
||||
Files: []stub.ItemData{},
|
||||
Folders: []stub.ItemData{
|
||||
{Name: folderAName},
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: folderAPath,
|
||||
Files: fileSet,
|
||||
Folders: []stub.ItemData{
|
||||
{Name: folderAName},
|
||||
{Name: folderBName},
|
||||
{Name: folderCName},
|
||||
},
|
||||
Meta: stub.MetaData{
|
||||
LinkShares: []stub.LinkShareData{
|
||||
{
|
||||
EntityIDs: []string{tertiaryUserID},
|
||||
Scope: "anonymous",
|
||||
Type: "edit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderAAPath,
|
||||
Files: fileSet,
|
||||
Meta: stub.MetaData{
|
||||
LinkShares: []stub.LinkShareData{
|
||||
{
|
||||
EntityIDs: []string{tertiaryUserID},
|
||||
Scope: "users",
|
||||
Type: "edit",
|
||||
},
|
||||
},
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderABPath,
|
||||
Files: fileSet,
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeInherited,
|
||||
},
|
||||
},
|
||||
{
|
||||
PathElements: subfolderACPath,
|
||||
Files: fileSet,
|
||||
Perms: stub.PermData{
|
||||
Meta: stub.MetaData{
|
||||
SharingMode: metadata.SharingModeCustom,
|
||||
},
|
||||
},
|
||||
|
||||
@ -195,6 +195,14 @@ func (h libraryRestoreHandler) PostItemPermissionUpdate(
|
||||
return h.ac.PostItemPermissionUpdate(ctx, driveID, itemID, body)
|
||||
}
|
||||
|
||||
func (h libraryRestoreHandler) PostItemLinkShareUpdate(
|
||||
ctx context.Context,
|
||||
driveID, itemID string,
|
||||
body *drives.ItemItemsItemCreateLinkPostRequestBody,
|
||||
) (models.Permissionable, error) {
|
||||
return h.ac.PostItemLinkShareUpdate(ctx, driveID, itemID, body)
|
||||
}
|
||||
|
||||
func (h libraryRestoreHandler) PostItemInContainer(
|
||||
ctx context.Context,
|
||||
driveID, parentFolderID string,
|
||||
|
||||
@ -30,7 +30,10 @@ type Drives struct {
|
||||
// Folders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
|
||||
const (
|
||||
itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
|
||||
createLinkShareURLFmt = "https://graph.microsoft.com/beta/drives/%s/items/%s/createLink"
|
||||
)
|
||||
|
||||
var ErrFolderNotFound = clues.New("folder not found")
|
||||
|
||||
@ -307,6 +310,27 @@ func (c Drives) DeleteItemPermission(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Drives) PostItemLinkShareUpdate(
|
||||
ctx context.Context,
|
||||
driveID, itemID string,
|
||||
body *drives.ItemItemsItemCreateLinkPostRequestBody,
|
||||
) (models.Permissionable, error) {
|
||||
ctx = graph.ConsumeNTokens(ctx, graph.PermissionsLC)
|
||||
|
||||
// We are using the beta version of the endpoint. This allows us
|
||||
// to add recipients in the same request as well as to make it not
|
||||
// send out and email for every link share the user gets added to.
|
||||
rawURL := fmt.Sprintf(createLinkShareURLFmt, driveID, itemID)
|
||||
builder := drives.NewItemItemsItemCreateLinkRequestBuilder(rawURL, c.Stable.Adapter())
|
||||
|
||||
itm, err := builder.Post(ctx, body, nil)
|
||||
if err != nil {
|
||||
return nil, graph.Wrap(ctx, err, "creating link share")
|
||||
}
|
||||
|
||||
return itm, nil
|
||||
}
|
||||
|
||||
// DriveItemCollisionKeyy constructs a key from the item name.
|
||||
// collision keys are used to identify duplicate item conflicts for handling advanced restoration config.
|
||||
func DriveItemCollisionKey(item models.DriveItemable) string {
|
||||
|
||||
@ -26,3 +26,11 @@ included in backup and restore.
|
||||
* SharePoint document library data can't be restored after the library has been deleted.
|
||||
|
||||
* Sharing information of items in OneDrive/SharePoint using sharing links aren't backed up and restored.
|
||||
|
||||
* Permissions/Access given to a site group can't be restored
|
||||
|
||||
* If a link share is created for an item with inheritance disabled
|
||||
(via the Graph API), the link shares restored in that item will
|
||||
not be inheritable by children
|
||||
|
||||
* Link shares with password protection can't be restored
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user