three changes 1. bumps the xsync package to v3 2. creates a common package for synced maps 3. replaces all xsync MapOf imports with the new common/syncd package. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🧹 Tech Debt/Cleanup #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
508 lines
14 KiB
Go
508 lines
14 KiB
Go
package drive
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/drives"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
|
"github.com/alcionai/corso/src/internal/common/syncd"
|
|
"github.com/alcionai/corso/src/internal/data"
|
|
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
|
|
"github.com/alcionai/corso/src/internal/version"
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
"github.com/alcionai/corso/src/pkg/logger"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
)
|
|
|
|
func getParentMetadata(
|
|
parentPath path.Path,
|
|
parentDirToMeta syncd.MapTo[metadata.Metadata],
|
|
) (metadata.Metadata, error) {
|
|
parentMeta, ok := parentDirToMeta.Load(parentPath.String())
|
|
if !ok {
|
|
drivePath, err := path.ToDrivePath(parentPath)
|
|
if err != nil {
|
|
return metadata.Metadata{}, clues.Wrap(err, "invalid restore path")
|
|
}
|
|
|
|
if len(drivePath.Folders) != 0 {
|
|
return metadata.Metadata{}, clues.Wrap(err, "computing item permissions")
|
|
}
|
|
|
|
parentMeta = metadata.Metadata{}
|
|
}
|
|
|
|
return parentMeta, nil
|
|
}
|
|
|
|
func getCollectionMetadata(
|
|
ctx context.Context,
|
|
drivePath *path.DrivePath,
|
|
dc data.RestoreCollection,
|
|
caches *restoreCaches,
|
|
backupVersion int,
|
|
restorePerms bool,
|
|
) (metadata.Metadata, error) {
|
|
if !restorePerms || backupVersion < version.OneDrive1DataAndMetaFiles {
|
|
return metadata.Metadata{}, nil
|
|
}
|
|
|
|
var (
|
|
err error
|
|
fullPath = dc.FullPath()
|
|
)
|
|
|
|
if len(drivePath.Folders) == 0 {
|
|
// No permissions for root folder
|
|
return metadata.Metadata{}, nil
|
|
}
|
|
|
|
if backupVersion < version.OneDrive4DirIncludesPermissions {
|
|
colMeta, err := getParentMetadata(fullPath, caches.ParentDirToMeta)
|
|
if err != nil {
|
|
return metadata.Metadata{}, clues.Wrap(err, "collection metadata")
|
|
}
|
|
|
|
return colMeta, nil
|
|
}
|
|
|
|
folders := fullPath.Folders()
|
|
metaName := folders[len(folders)-1] + metadata.DirMetaFileSuffix
|
|
|
|
if backupVersion >= version.OneDrive5DirMetaNoName {
|
|
metaName = metadata.DirMetaFileSuffix
|
|
}
|
|
|
|
meta, err := FetchAndReadMetadata(ctx, dc, metaName)
|
|
if err != nil {
|
|
return metadata.Metadata{}, clues.Wrap(err, "collection metadata")
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// 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 syncd.MapTo[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.NewWC(ctx, "getting parent")
|
|
}
|
|
|
|
for len(parent.Elements()) > 0 {
|
|
ictx := clues.Add(ctx, "current_ancestor_dir", parent)
|
|
|
|
drivePath, err := path.ToDrivePath(parent)
|
|
if err != nil {
|
|
return nil, clues.NewWC(ictx, "transforming dir to drivePath")
|
|
}
|
|
|
|
if len(drivePath.Folders) == 0 {
|
|
break
|
|
}
|
|
|
|
meta, ok := parentMetas.Load(parent.String())
|
|
if !ok {
|
|
return nil, clues.NewWC(ictx, "no metadata found in parent")
|
|
}
|
|
|
|
// 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.NewWC(ictx, "getting parent")
|
|
}
|
|
}
|
|
|
|
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 computePreviousMetadata(
|
|
ctx context.Context,
|
|
originDir path.Path,
|
|
// map parent dir -> parent's metadata
|
|
parentMetas syncd.MapTo[metadata.Metadata],
|
|
) (metadata.Metadata, error) {
|
|
var (
|
|
parent path.Path
|
|
meta metadata.Metadata
|
|
|
|
err error
|
|
ok bool
|
|
)
|
|
|
|
parent = originDir
|
|
|
|
for {
|
|
parent, err = parent.Dir()
|
|
if err != nil {
|
|
return metadata.Metadata{}, clues.NewWC(ctx, "getting parent")
|
|
}
|
|
|
|
ictx := clues.Add(ctx, "parent_dir", parent)
|
|
|
|
drivePath, err := path.ToDrivePath(parent)
|
|
if err != nil {
|
|
return metadata.Metadata{}, clues.NewWC(ictx, "transforming dir to drivePath")
|
|
}
|
|
|
|
if len(drivePath.Folders) == 0 {
|
|
return metadata.Metadata{}, nil
|
|
}
|
|
|
|
meta, ok = parentMetas.Load(parent.String())
|
|
if !ok {
|
|
return metadata.Metadata{}, clues.NewWC(ictx, "no metadata found for parent folder: "+parent.String())
|
|
}
|
|
|
|
if meta.SharingMode == metadata.SharingModeCustom {
|
|
return meta, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
type updateDeleteItemPermissioner interface {
|
|
DeleteItemPermissioner
|
|
UpdateItemPermissioner
|
|
}
|
|
|
|
// UpdatePermissions takes in the set of permission to be added and
|
|
// removed from an item to bring it to the desired state.
|
|
func UpdatePermissions(
|
|
ctx context.Context,
|
|
udip updateDeleteItemPermissioner,
|
|
driveID string,
|
|
itemID string,
|
|
permAdded, permRemoved []metadata.Permission,
|
|
oldPermIDToNewID syncd.MapTo[string],
|
|
errs *fault.Bus,
|
|
) error {
|
|
el := errs.Local()
|
|
|
|
// The ordering of the operations is important here. We first
|
|
// remove all the removed permissions and then add the added ones.
|
|
for _, p := range permRemoved {
|
|
ictx := clues.Add(
|
|
ctx,
|
|
"permission_entity_type", p.EntityType,
|
|
"permission_entity_id", clues.Hide(p.EntityID))
|
|
|
|
// deletes require unique http clients
|
|
// https://github.com/alcionai/corso/issues/2707
|
|
// this is bad citizenship, and could end up consuming a lot of
|
|
// system resources if servicers leak client connections (sockets, etc).
|
|
|
|
pid, ok := oldPermIDToNewID.Load(p.ID)
|
|
if !ok {
|
|
return clues.NewWC(ictx, "no new permission id")
|
|
}
|
|
|
|
err := udip.DeleteItemPermission(
|
|
ictx,
|
|
driveID,
|
|
itemID,
|
|
pid)
|
|
if err != nil {
|
|
return clues.Stack(err)
|
|
}
|
|
}
|
|
|
|
for _, p := range permAdded {
|
|
if el.Failure() != nil {
|
|
break
|
|
}
|
|
|
|
ictx := clues.Add(
|
|
ctx,
|
|
"permission_entity_type", p.EntityType,
|
|
"permission_entity_id", clues.Hide(p.EntityID))
|
|
|
|
// We are not able to restore permissions when there are no
|
|
// roles or for owner, this seems to be restriction in graph
|
|
roles := []string{}
|
|
|
|
for _, r := range p.Roles {
|
|
if r != "owner" {
|
|
roles = append(roles, r)
|
|
}
|
|
}
|
|
|
|
// 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 len(roles) == 0 || p.EntityType == metadata.GV2SiteGroup {
|
|
continue
|
|
}
|
|
|
|
pbody := drives.NewItemItemsItemInvitePostRequestBody()
|
|
pbody.SetRoles(roles)
|
|
|
|
if p.Expiration != nil {
|
|
expiry := p.Expiration.String()
|
|
pbody.SetExpirationDateTime(&expiry)
|
|
}
|
|
|
|
pbody.SetSendInvitation(ptr.To(false))
|
|
pbody.SetRequireSignIn(ptr.To(true))
|
|
|
|
rec := models.NewDriveRecipient()
|
|
if len(p.EntityID) > 0 {
|
|
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})
|
|
|
|
newPerm, err := udip.PostItemPermissionUpdate(ictx, driveID, itemID, pbody)
|
|
if graph.IsErrUsersCannotBeResolved(err) {
|
|
logger.CtxErr(ictx, err).Info("Unable to restore link share")
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
el.AddRecoverable(ictx, clues.Stack(err))
|
|
continue
|
|
}
|
|
|
|
oldPermIDToNewID.Store(p.ID, ptr.Val(newPerm.GetValue()[0].GetId()))
|
|
}
|
|
|
|
return el.Failure()
|
|
}
|
|
|
|
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 syncd.MapTo[string],
|
|
errs *fault.Bus,
|
|
) (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
|
|
el := errs.Local()
|
|
|
|
for _, ls := range lsAdded {
|
|
if el.Failure() != nil {
|
|
break
|
|
}
|
|
|
|
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.SetTypeEscaped(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 graph.IsErrUsersCannotBeResolved(err) {
|
|
logger.CtxErr(ictx, err).Info("Unable to restore link share")
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
el.AddRecoverable(ctx, clues.Stack(err))
|
|
continue
|
|
}
|
|
|
|
oldLinkShareIDToNewID.Store(ls.ID, ptr.Val(newLS.GetId()))
|
|
}
|
|
|
|
if el.Failure() != nil {
|
|
return alreadyDeleted, el.Failure()
|
|
}
|
|
|
|
// 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.SetTypeEscaped(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
|
|
// on onedrive items.
|
|
func RestorePermissions(
|
|
ctx context.Context,
|
|
rh RestoreHandler,
|
|
driveID string,
|
|
itemID string,
|
|
itemPath path.Path,
|
|
current metadata.Metadata,
|
|
caches *restoreCaches,
|
|
errs *fault.Bus,
|
|
) error {
|
|
if current.SharingMode == metadata.SharingModeInherited {
|
|
return nil
|
|
}
|
|
|
|
ctx = clues.Add(ctx, "permission_item_id", itemID)
|
|
|
|
previousLinkShares, err := computePreviousLinkShares(ctx, itemPath, caches.ParentDirToMeta)
|
|
if err != nil {
|
|
return clues.Wrap(err, "previous link shares")
|
|
}
|
|
|
|
lsAdded, lsRemoved := metadata.DiffLinkShares(previousLinkShares, current.LinkShares)
|
|
|
|
// 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,
|
|
errs)
|
|
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,
|
|
itemID,
|
|
permAdded,
|
|
permRemoved,
|
|
caches.OldPermIDToNewID,
|
|
errs)
|
|
if err != nil {
|
|
return clues.Wrap(err, "updating permissions")
|
|
}
|
|
|
|
return nil
|
|
}
|