Keepers f4ed4d7250
bump xsync to v3 (#4704)
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
2023-11-17 18:57:29 +00:00

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
}