From 9d321bf3790df25d6abf29f0902e40c3d05c3b1c Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 7 Feb 2024 11:59:54 +0100 Subject: [PATCH] enhancement(sharing): Return newly created driveItem When accepting a share via 'POST /v1beta1/drives/{driveId}/root/children' return the newly created driveItem. This driveItem wraps the accepted remoteItem representing the shared resource (similar to the 'sharedWithMe' response. This also refactors some of the helpers for user lookup and CS3 share to driveItem conversion so they can be more easily shared. --- .../pkg/service/v0/api_drives_drive_item.go | 66 +++- .../service/v0/api_drives_drive_item_test.go | 138 ++++++- services/graph/pkg/service/v0/drives.go | 8 +- services/graph/pkg/service/v0/service.go | 2 +- services/graph/pkg/service/v0/sharedbyme.go | 13 +- services/graph/pkg/service/v0/sharedwithme.go | 339 +---------------- services/graph/pkg/service/v0/utils.go | 348 +++++++++++++++++- 7 files changed, 534 insertions(+), 380 deletions(-) diff --git a/services/graph/pkg/service/v0/api_drives_drive_item.go b/services/graph/pkg/service/v0/api_drives_drive_item.go index beab116bf..1c799eeec 100644 --- a/services/graph/pkg/service/v0/api_drives_drive_item.go +++ b/services/graph/pkg/service/v0/api_drives_drive_item.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "path/filepath" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -14,10 +15,10 @@ import ( "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/storagespace" - "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/identity" ) const ( @@ -33,15 +34,19 @@ type DrivesDriveItemProvider interface { // DrivesDriveItemService contains the production business logic for everything that relates to drives type DrivesDriveItemService struct { - logger log.Logger - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + logger log.Logger + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + identityCache identity.IdentityCache + resharingEnabled bool } // NewDrivesDriveItemService creates a new DrivesDriveItemService -func NewDrivesDriveItemService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (DrivesDriveItemService, error) { +func NewDrivesDriveItemService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, resharing bool) (DrivesDriveItemService, error) { return DrivesDriveItemService{ - logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()}, - gatewaySelector: gatewaySelector, + logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()}, + gatewaySelector: gatewaySelector, + identityCache: identityCache, + resharingEnabled: resharing, }, nil } @@ -96,11 +101,17 @@ func (s DrivesDriveItemService) UnmountShare(ctx context.Context, resourceID sto // MountShare mounts a share func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID storageprovider.ResourceId, name string) (libregraph.DriveItem, error) { + if filepath.IsAbs(name) { + return libregraph.DriveItem{}, errorcode.New(errorcode.InvalidRequest, "name cannot be an absolute path") + } + name = filepath.Clean(name) + gatewayClient, err := s.gatewaySelector.Next() if err != nil { return libregraph.DriveItem{}, err } + // Get all shares that the user has received for this resource. There might be multiple receivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{ Filters: []*collaboration.Filter{ { @@ -129,6 +140,10 @@ func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID stora var errs []error + var acceptedShares []*collaboration.ReceivedShare + + // try to accept all of the received shares for this resource. So that the stat is in sync across all + // shares for _, receivedShare := range receivedSharesResponse.GetShares() { updateMask := &fieldmaskpb.FieldMask{Paths: []string{_fieldMaskPathState}} receivedShare.State = collaboration.ShareState_SHARE_STATE_ACCEPTED @@ -140,9 +155,8 @@ func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID stora mountPoint = &storageprovider.Reference{} } - newPath := utils.MakeRelativePath(name) - if mountPoint.GetPath() != newPath { - mountPoint.Path = newPath + if filepath.Clean(mountPoint.GetPath()) != name { + mountPoint.Path = name receivedShare.MountPoint = mountPoint updateMask.Paths = append(updateMask.Paths, _fieldMaskPathMountPoint) } @@ -154,17 +168,35 @@ func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID stora } updateReceivedShareResponse, err := gatewayClient.UpdateReceivedShare(ctx, updateReceivedShareRequest) - if err != nil { - errs = append(errs, err) - continue + switch errCode := errorcode.FromCS3Status(updateReceivedShareResponse.GetStatus(), err); { + case errCode == nil: + acceptedShares = append(acceptedShares, updateReceivedShareResponse.GetShare()) + default: + // Just log at debug level here. If a single accept for any of the received shares failed this + // is not a critical problem. We mainly need to handle the case where all accepts fail. (Outside + // the loop) + s.logger.Debug().Err(errCode). + Str("shareid", receivedShare.GetShare().GetId().String()). + Str("resourceid", receivedShare.GetShare().GetResourceId().String()). + Msg("failed to accept share") + errs = append(errs, errCode) } - - // fixMe: send to nirvana, wait for toDriverItem func - _ = updateReceivedShareResponse } - // fixMe: return a concrete driveItem - return libregraph.DriveItem{}, errors.Join(errs...) + if len(receivedSharesResponse.GetShares()) == len(errs) { + // none of the received shares could be accepted. This is an error. Return it. + return libregraph.DriveItem{}, errors.Join(errs...) + } + + // As the accepted shares are all for the same resource they should collapse to a single driveitem + items, err := cs3ReceivedSharesToDriveItems(ctx, &s.logger, gatewayClient, s.identityCache, s.resharingEnabled, acceptedShares) + switch { + case err != nil: + return libregraph.DriveItem{}, nil + case len(items) != 1: + return libregraph.DriveItem{}, errorcode.New(errorcode.GeneralException, "failed to convert accepted shares into driveitem") + } + return items[0], nil } // DrivesDriveItemApi is the api that registers the http endpoints which expose needed operation to the graph api. diff --git a/services/graph/pkg/service/v0/api_drives_drive_item_test.go b/services/graph/pkg/service/v0/api_drives_drive_item_test.go index 24c90e7f8..9d5fa90e5 100644 --- a/services/graph/pkg/service/v0/api_drives_drive_item_test.go +++ b/services/graph/pkg/service/v0/api_drives_drive_item_test.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strconv" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -20,10 +21,12 @@ import ( "github.com/tidwall/gjson" "google.golang.org/grpc" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/storagespace" cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/identity" svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" ) @@ -41,7 +44,9 @@ var _ = Describe("DrivesDriveItemService", func() { gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT()) gatewaySelector.On("Next").Return(gatewayClient, nil) - service, err := svc.NewDrivesDriveItemService(logger, gatewaySelector) + cache := identity.NewIdentityCache(identity.IdentityCacheWithGatewaySelector(gatewaySelector)) + + service, err := svc.NewDrivesDriveItemService(logger, gatewaySelector, cache, false) Expect(err).ToNot(HaveOccurred()) drivesDriveItemService = service }) @@ -111,7 +116,12 @@ var _ = Describe("DrivesDriveItemService", func() { Describe("gateway client share update", func() { It("updates the share state to be accepted", func() { expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "1$2!3", + OpaqueId: "1:2:3", + } + expectedResourceID := storageprovider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", } gatewayClient. @@ -135,14 +145,42 @@ var _ = Describe("DrivesDriveItemService", func() { Expect(in.GetUpdateMask().GetPaths()).To(Equal([]string{"state"})) Expect(in.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED)) Expect(in.GetShare().GetShare().GetId().GetOpaqueId()).To(Equal(expectedShareID.GetOpaqueId())) - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil + return &collaborationv1beta1.UpdateReceivedShareResponse{ + Status: status.NewOK(ctx), + Share: &collaborationv1beta1.ReceivedShare{ + State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaborationv1beta1.Share{ + Id: &expectedShareID, + ResourceId: &expectedResourceID, + }, + }, + }, nil + }) + gatewayClient. + On("Stat", mock.Anything, mock.Anything, mock.Anything). + Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { + return &storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &storageprovider.ResourceInfo{ + Id: &expectedResourceID, + Name: "name", + }, + }, nil }) - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "") Expect(err).ToNot(HaveOccurred()) }) It("updates the mountPoint", func() { + expectedShareID := collaborationv1beta1.ShareId{ + OpaqueId: "1:2:3", + } + expectedResourceID := storageprovider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + } + gatewayClient. On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { @@ -158,15 +196,45 @@ var _ = Describe("DrivesDriveItemService", func() { Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { Expect(in.GetUpdateMask().GetPaths()).To(HaveLen(2)) Expect(in.GetUpdateMask().GetPaths()).To(ContainElements("mount_point")) - Expect(in.GetShare().GetMountPoint().GetPath()).To(Equal("./new name")) - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil + Expect(in.GetShare().GetMountPoint().GetPath()).To(Equal("new name")) + return &collaborationv1beta1.UpdateReceivedShareResponse{ + Status: status.NewOK(ctx), + Share: &collaborationv1beta1.ReceivedShare{ + State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaborationv1beta1.Share{ + Id: &expectedShareID, + ResourceId: &expectedResourceID, + }, + MountPoint: &storageprovider.Reference{ + Path: "new name", + }, + }, + }, nil + }) + gatewayClient. + On("Stat", mock.Anything, mock.Anything, mock.Anything). + Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { + return &storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &storageprovider.ResourceInfo{ + Id: &expectedResourceID, + Name: "name", + }, + }, nil }) - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") + di, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") Expect(err).ToNot(HaveOccurred()) + Expect(di.GetName()).To(Equal("new name")) }) - It("bubbles errors and continues", func() { + It("succeeds when any of the shares was accepted", func() { + expectedResourceID := storageprovider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + } + gatewayClient. On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { @@ -190,11 +258,61 @@ var _ = Describe("DrivesDriveItemService", func() { return nil, fmt.Errorf("error %d", calls) } - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil + return &collaborationv1beta1.UpdateReceivedShareResponse{ + Status: status.NewOK(ctx), + Share: &collaborationv1beta1.ReceivedShare{ + State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaborationv1beta1.Share{ + Id: &collaborationv1beta1.ShareId{ + OpaqueId: strconv.Itoa(calls), + }, + ResourceId: &expectedResourceID, + }, + }, + }, nil + }) + gatewayClient. + On("Stat", mock.Anything, mock.Anything, mock.Anything). + Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { + return &storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &storageprovider.ResourceInfo{ + Id: &expectedResourceID, + Name: "name", + }, + }, nil + }) + + di, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") + Expect(err).To(BeNil()) + Expect(di.GetId()).ToNot(BeEmpty()) + }) + It("errors when none of the shares can be accepted", func() { + gatewayClient. + On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). + Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + return &collaborationv1beta1.ListReceivedSharesResponse{ + Shares: []*collaborationv1beta1.ReceivedShare{ + {}, + {}, + {}, + }, + }, nil + }) + + var calls int + gatewayClient. + On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). + Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + calls++ + Expect(calls).To(BeNumerically("<=", 3)) + return nil, fmt.Errorf("error %d", calls) }) _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") - Expect(fmt.Sprint(err)).To(Equal("error 1\nerror 2")) + Expect(fmt.Sprint(err)).To(ContainSubstring("error 1")) + Expect(fmt.Sprint(err)).To(ContainSubstring("error 2")) + Expect(fmt.Sprint(err)).To(ContainSubstring("error 3")) }) }) }) diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go index 311c351af..cad1abb6a 100644 --- a/services/graph/pkg/service/v0/drives.go +++ b/services/graph/pkg/service/v0/drives.go @@ -932,17 +932,17 @@ func (g Graph) cs3PermissionsToLibreGraph(ctx context.Context, space *storagepro tmp := id var identitySet libregraph.IdentitySet if _, ok := groupsMap[id]; ok { - group, err := g.identityCache.GetGroup(ctx, tmp) + identity, err := groupIdToIdentity(ctx, g.identityCache, tmp) if err != nil { g.logger.Warn().Str("groupid", tmp).Msg("Group not found by id") } - identitySet = libregraph.IdentitySet{Group: &libregraph.Identity{Id: &tmp, DisplayName: group.GetDisplayName()}} + identitySet = libregraph.IdentitySet{Group: &identity} } else { - user, err := g.identityCache.GetUser(ctx, tmp) + identity, err := userIdToIdentity(ctx, g.identityCache, tmp) if err != nil { g.logger.Warn().Str("userid", tmp).Msg("User not found by id") } - identitySet = libregraph.IdentitySet{User: &libregraph.Identity{Id: &tmp, DisplayName: user.GetDisplayName()}} + identitySet = libregraph.IdentitySet{User: &identity} } p := libregraph.Permission{ diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index b1abfc736..25c19b2dc 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -204,7 +204,7 @@ func NewService(opts ...Option) (Graph, error) { requireAdmin = options.RequireAdminMiddleware } - drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) + drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector, identityCache, options.Config.FilesSharing.EnableResharing) if err != nil { return svc, err } diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index 0d308b847..340a0b0cf 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -145,10 +145,9 @@ func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboratio perm.SetRoles([]string{}) perm.SetId(share.Id.OpaqueId) grantedTo := libregraph.SharePointIdentitySet{} - var li libregraph.Identity switch share.GetGrantee().GetType() { case storageprovider.GranteeType_GRANTEE_TYPE_USER: - user, err := g.identityCache.GetUser(ctx, share.Grantee.GetUserId().GetOpaqueId()) + user, err := cs3UserIdToIdentity(ctx, g.identityCache, share.Grantee.GetUserId()) switch { case errors.Is(err, identity.ErrNotFound): g.logger.Warn().Str("userid", share.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id") @@ -157,12 +156,10 @@ func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboratio case err != nil: return nil, errorcode.New(errorcode.GeneralException, err.Error()) default: - li.SetDisplayName(user.GetDisplayName()) - li.SetId(user.GetId()) - grantedTo.SetUser(li) + grantedTo.SetUser(user) } case storageprovider.GranteeType_GRANTEE_TYPE_GROUP: - group, err := g.identityCache.GetGroup(ctx, share.Grantee.GetGroupId().GetOpaqueId()) + group, err := groupIdToIdentity(ctx, g.identityCache, share.Grantee.GetGroupId().GetOpaqueId()) switch { case errors.Is(err, identity.ErrNotFound): g.logger.Warn().Str("groupid", share.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id") @@ -171,9 +168,7 @@ func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboratio case err != nil: return nil, errorcode.New(errorcode.GeneralException, err.Error()) default: - li.SetDisplayName(group.GetDisplayName()) - li.SetId(group.GetId()) - grantedTo.SetGroup(li) + grantedTo.SetGroup(group) } } diff --git a/services/graph/pkg/service/v0/sharedwithme.go b/services/graph/pkg/service/v0/sharedwithme.go index 247ce0d15..e23ca660b 100644 --- a/services/graph/pkg/service/v0/sharedwithme.go +++ b/services/graph/pkg/service/v0/sharedwithme.go @@ -3,20 +3,12 @@ package svc import ( "context" "net/http" - "reflect" - cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" - "golang.org/x/sync/errgroup" - - "github.com/cs3org/reva/v2/pkg/storagespace" - "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" - "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" ) // ListSharedWithMe lists the files shared with the current user. @@ -47,334 +39,5 @@ func (g Graph) listSharedWithMe(ctx context.Context) ([]libregraph.DriveItem, er return nil, *errCode } - return g.cs3ReceivedSharesToDriveItems(ctx, listReceivedSharesResponse.GetShares()) -} - -func (g Graph) cs3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) { - gatewayClient, err := g.gatewaySelector.Next() - if err != nil { - g.logger.Error().Err(err).Msg("could not select next gateway client") - return nil, err - } - - // doStat is a helper function that stat a resource. - doStat := func(resourceId *storageprovider.ResourceId) (*storageprovider.StatResponse, error) { - shareStat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{ - Ref: &storageprovider.Reference{ResourceId: resourceId}, - }) - switch errCode := errorcode.FromCS3Status(shareStat.GetStatus(), err); { - case errCode == nil: - break - // skip ItemNotFound shares, they might have been deleted in the meantime or orphans. - case errCode.GetCode() == errorcode.ItemNotFound: - return nil, nil - default: - g.logger.Error().Err(errCode).Msg("could not stat") - return nil, errCode - } - - return shareStat, nil - } - - ch := make(chan libregraph.DriveItem) - group := new(errgroup.Group) - // Set max concurrency - group.SetLimit(10) - - receivedSharesByResourceID := make(map[string][]*collaboration.ReceivedShare, len(receivedShares)) - for _, receivedShare := range receivedShares { - rIDStr := storagespace.FormatResourceID(*receivedShare.GetShare().GetResourceId()) - receivedSharesByResourceID[rIDStr] = append(receivedSharesByResourceID[rIDStr], receivedShare) - } - - for _, receivedSharesForResource := range receivedSharesByResourceID { - receivedShares := receivedSharesForResource - - group.Go(func() error { - var err error // redeclare - shareStat, err := doStat(receivedShares[0].GetShare().GetResourceId()) - if shareStat == nil || err != nil { - return err - } - - driveItem := libregraph.NewDriveItem() - - permissions := make([]libregraph.Permission, 0, len(receivedShares)) - - var oldestReceivedShare *collaboration.ReceivedShare - for _, receivedShare := range receivedShares { - switch { - case oldestReceivedShare == nil: - fallthrough - case utils.TSToTime(receivedShare.GetShare().GetCtime()).Before(utils.TSToTime(oldestReceivedShare.GetShare().GetCtime())): - oldestReceivedShare = receivedShare - } - - permission, err := g.cs3ReceivedShareToLibreGraphPermissions(ctx, receivedShare) - if err != nil { - return err - } - - // If at least one of the shares was accepted, we consider the driveItem's synchronized - // flag enabled. - // Also we use the Mountpoint name of the first accepted mountpoint as the name of - // of the driveItem - if receivedShare.GetState() == collaboration.ShareState_SHARE_STATE_ACCEPTED { - driveItem.SetClientSynchronize(true) - if name := receivedShare.GetMountPoint().GetPath(); name != "" && driveItem.GetName() == "" { - driveItem.SetName(receivedShare.GetMountPoint().GetPath()) - } - } - - // if at least one share is marked as hidden, consider the whole driveItem to be hidden - if receivedShare.GetHidden() { - driveItem.SetUIHidden(true) - } - - if userID := receivedShare.GetShare().GetCreator(); userID != nil { - identity, err := g.cs3UserIdToIdentity(ctx, userID) - if err != nil { - g.logger.Warn().Err(err).Str("userid", userID.String()).Msg("could not get creator of the share") - } - - permission.SetInvitation( - libregraph.SharingInvitation{ - InvitedBy: &libregraph.IdentitySet{ - User: &identity, - }, - }, - ) - } - permissions = append(permissions, *permission) - - } - - // To stay compatible with the usershareprovider and the webdav - // service the id of the driveItem is composed of the StorageID and - // SpaceID of the sharestorage appended with the opaque ID of - // the oldest share for the resource: - // '$! - // Note: This means that the driveitem ID will change when the oldest - // shared is removed. It would be good to have are more stable ID here (e.g. - // derived from the shared resource's ID. But as we need to use the same - // ID across all services this means we needed to make similar adjustments - // to the sharejail (usershareprovider, webdav). Which we can't currently do - // as some clients rely on the IDs used there having a special format. - driveItem.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ - StorageId: utils.ShareStorageProviderID, - OpaqueId: oldestReceivedShare.GetShare().GetId().GetOpaqueId(), - SpaceId: utils.ShareStorageSpaceID, - })) - - if !driveItem.HasUIHidden() { - driveItem.SetUIHidden(false) - } - if !driveItem.HasClientSynchronize() { - driveItem.SetClientSynchronize(false) - if name := shareStat.GetInfo().GetName(); name != "" { - driveItem.SetName(name) - } - } - - remoteItem := libregraph.NewRemoteItem() - { - if id := shareStat.GetInfo().GetId(); id != nil { - remoteItem.SetId(storagespace.FormatResourceID(*id)) - } - - if name := shareStat.GetInfo().GetName(); name != "" { - remoteItem.SetName(name) - } - - if etag := shareStat.GetInfo().GetEtag(); etag != "" { - remoteItem.SetETag(etag) - } - - if mTime := shareStat.GetInfo().GetMtime(); mTime != nil { - remoteItem.SetLastModifiedDateTime(cs3TimestampToTime(mTime)) - } - - if size := shareStat.GetInfo().GetSize(); size != 0 { - remoteItem.SetSize(int64(size)) - } - - parentReference := libregraph.NewItemReference() - if spaceType := shareStat.GetInfo().GetSpace().GetSpaceType(); spaceType != "" { - parentReference.SetDriveType(spaceType) - } - - if root := shareStat.GetInfo().GetSpace().GetRoot(); root != nil { - parentReference.SetDriveId(storagespace.FormatResourceID(*root)) - } - if !reflect.ValueOf(*parentReference).IsZero() { - remoteItem.ParentReference = parentReference - } - - } - - // the parentReference of the outer driveItem should be the drive - // containing the mountpoint i.e. the share jail - driveItem.ParentReference = libregraph.NewItemReference() - driveItem.ParentReference.SetDriveType("virtual") - driveItem.ParentReference.SetDriveId(storagespace.FormatStorageID(utils.ShareStorageProviderID, utils.ShareStorageSpaceID)) - driveItem.ParentReference.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ - StorageId: utils.ShareStorageProviderID, - OpaqueId: utils.ShareStorageSpaceID, - SpaceId: utils.ShareStorageSpaceID, - })) - if etag := shareStat.GetInfo().GetEtag(); etag != "" { - driveItem.SetETag(etag) - } - - // connect the dots - { - if mTime := shareStat.GetInfo().GetMtime(); mTime != nil { - t := cs3TimestampToTime(mTime) - - driveItem.SetLastModifiedDateTime(t) - remoteItem.SetLastModifiedDateTime(t) - } - - if size := shareStat.GetInfo().GetSize(); size != 0 { - s := int64(size) - - driveItem.SetSize(s) - remoteItem.SetSize(s) - } - - if userID := shareStat.GetInfo().GetOwner(); userID != nil && userID.Type != cs3User.UserType_USER_TYPE_SPACE_OWNER { - identity, err := g.cs3UserIdToIdentity(ctx, userID) - if err != nil { - // TODO: define a proper error behavior here. We don't - // want the whole request to fail just because a single - // resource owner couldn't be resolved. But, should be - // really return the affect share in the response? - // For now we just log a warning. The returned - // identitySet will just contain the userid. - g.logger.Warn().Err(err).Str("userid", userID.String()).Msg("could not get owner of shared resource") - } - - remoteItem.SetCreatedBy(libregraph.IdentitySet{User: &identity}) - driveItem.SetCreatedBy(libregraph.IdentitySet{User: &identity}) - } - switch info := shareStat.GetInfo(); { - case info.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER: - folder := libregraph.NewFolder() - - remoteItem.Folder = folder - driveItem.Folder = folder - case info.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_FILE: - file := libregraph.NewOpenGraphFile() - - if mimeType := info.GetMimeType(); mimeType != "" { - file.MimeType = &mimeType - } - - remoteItem.File = file - driveItem.File = file - } - - if len(permissions) > 0 { - remoteItem.Permissions = permissions - } - - if !reflect.ValueOf(*remoteItem).IsZero() { - driveItem.RemoteItem = remoteItem - } - } - - ch <- *driveItem - - return nil - }) - } - - // wait for concurrent requests to finish - go func() { - err = group.Wait() - close(ch) - }() - - driveItems := make([]libregraph.DriveItem, 0, len(receivedSharesByResourceID)) - for di := range ch { - driveItems = append(driveItems, di) - } - - return driveItems, err -} - -func (g Graph) cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, receivedShare *collaboration.ReceivedShare) (*libregraph.Permission, error) { - permission := libregraph.NewPermission() - if id := receivedShare.GetShare().GetId().GetOpaqueId(); id != "" { - permission.SetId(id) - } - - if expiration := receivedShare.GetShare().GetExpiration(); expiration != nil { - permission.SetExpirationDateTime(cs3TimestampToTime(expiration)) - } - - if permissionSet := receivedShare.GetShare().GetPermissions().GetPermissions(); permissionSet != nil { - role := unifiedrole.CS3ResourcePermissionsToUnifiedRole( - *permissionSet, - unifiedrole.UnifiedRoleConditionGrantee, - g.config.FilesSharing.EnableResharing, - ) - - if role != nil { - permission.SetRoles([]string{role.GetId()}) - } - - actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*permissionSet) - - // actions only make sense if no role is set - if role == nil && len(actions) > 0 { - permission.SetLibreGraphPermissionsActions(actions) - } - } - - switch grantee := receivedShare.GetShare().GetGrantee(); { - case grantee.GetType() == storageprovider.GranteeType_GRANTEE_TYPE_USER: - user, err := g.identityCache.GetUser(ctx, grantee.GetUserId().GetOpaqueId()) - if err != nil { - g.logger.Error().Err(err).Msg("could not get user") - return nil, err - } - - permission.SetGrantedToV2(libregraph.SharePointIdentitySet{ - User: &libregraph.Identity{ - DisplayName: user.GetDisplayName(), - Id: user.Id, - }, - }) - case grantee.GetType() == storageprovider.GranteeType_GRANTEE_TYPE_GROUP: - group, err := g.identityCache.GetGroup(ctx, grantee.GetGroupId().GetOpaqueId()) - if err != nil { - g.logger.Error().Err(err).Msg("could not get group") - return nil, err - } - - permission.SetGrantedToV2(libregraph.SharePointIdentitySet{ - Group: &libregraph.Identity{ - DisplayName: group.GetDisplayName(), - Id: group.Id, - }, - }) - } - - return permission, nil -} - -func (g Graph) cs3UserIdToIdentity(ctx context.Context, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { - identity := libregraph.Identity{ - Id: libregraph.PtrString(cs3UserID.GetOpaqueId()), - } - var err error - if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER { - var user libregraph.User - user, err = g.identityCache.GetUser(ctx, cs3UserID.GetOpaqueId()) - if err == nil { - identity.SetDisplayName(user.GetDisplayName()) - } - } - return identity, err + return cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, g.config.FilesSharing.EnableResharing, listReceivedSharesResponse.GetShares()) } diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index 0894cf18b..578c3d1cb 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -1,17 +1,25 @@ package svc import ( + "context" "encoding/json" "io" "net/http" + "reflect" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + "golang.org/x/sync/errgroup" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/log" - "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/identity" + "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" ) // StrictJSONUnmarshal is a wrapper around json.Unmarshal that returns an error if the json contains unknown fields. @@ -79,3 +87,341 @@ func (g Graph) GetGatewayClient(w http.ResponseWriter, r *http.Request) (gateway func IsShareJail(id storageprovider.ResourceId) bool { return id.GetStorageId() == utils.ShareStorageProviderID && id.GetSpaceId() == utils.ShareStorageSpaceID } + +// userIdToIdentity looks the user for the supplied id using the cache and returns it +// as a libregraph.Identity +func userIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID string) (libregraph.Identity, error) { + identity := libregraph.Identity{ + Id: libregraph.PtrString(userID), + } + user, err := cache.GetUser(ctx, userID) + if err == nil { + identity.SetDisplayName(user.GetDisplayName()) + } + return identity, err +} + +// cs3UserIdToIdentity looks up the user for the supplied cs3 userid using the cache and returns it +// as a libregraph.Identity. Skips the user lookup if the id type is USER_TYPE_SPACE_OWNER +func cs3UserIdToIdentity(ctx context.Context, cache identity.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { + if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER { + return userIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId()) + } + return libregraph.Identity{Id: libregraph.PtrString(cs3UserID.GetOpaqueId())}, nil +} + +// groupIdToIdentity looks up the group for the supplied cs3 groupid using the cache and returns it +// as a libregraph.Identity. +func groupIdToIdentity(ctx context.Context, cache identity.IdentityCache, groupID string) (libregraph.Identity, error) { + identity := libregraph.Identity{ + Id: libregraph.PtrString(groupID), + } + group, err := cache.GetGroup(ctx, groupID) + if err == nil { + identity.SetDisplayName(group.GetDisplayName()) + } + return identity, err +} + +func cs3ReceivedSharesToDriveItems(ctx context.Context, + logger *log.Logger, + gatewayClient gateway.GatewayAPIClient, + identityCache identity.IdentityCache, + resharing bool, + receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) { + + // doStat is a helper function that stat a resource. + doStat := func(resourceId *storageprovider.ResourceId) (*storageprovider.StatResponse, error) { + shareStat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{ + Ref: &storageprovider.Reference{ResourceId: resourceId}, + }) + switch errCode := errorcode.FromCS3Status(shareStat.GetStatus(), err); { + case errCode == nil: + break + // skip ItemNotFound shares, they might have been deleted in the meantime or orphans. + case errCode.GetCode() == errorcode.ItemNotFound: + return nil, nil + default: + logger.Error().Err(errCode).Msg("could not stat") + return nil, errCode + } + + return shareStat, nil + } + + ch := make(chan libregraph.DriveItem) + group := new(errgroup.Group) + // Set max concurrency + group.SetLimit(10) + + receivedSharesByResourceID := make(map[string][]*collaboration.ReceivedShare, len(receivedShares)) + for _, receivedShare := range receivedShares { + rIDStr := storagespace.FormatResourceID(*receivedShare.GetShare().GetResourceId()) + receivedSharesByResourceID[rIDStr] = append(receivedSharesByResourceID[rIDStr], receivedShare) + } + + for _, receivedSharesForResource := range receivedSharesByResourceID { + receivedShares := receivedSharesForResource + + group.Go(func() error { + var err error // redeclare + shareStat, err := doStat(receivedShares[0].GetShare().GetResourceId()) + if shareStat == nil || err != nil { + return err + } + + driveItem := libregraph.NewDriveItem() + + permissions := make([]libregraph.Permission, 0, len(receivedShares)) + + var oldestReceivedShare *collaboration.ReceivedShare + for _, receivedShare := range receivedShares { + switch { + case oldestReceivedShare == nil: + fallthrough + case utils.TSToTime(receivedShare.GetShare().GetCtime()).Before(utils.TSToTime(oldestReceivedShare.GetShare().GetCtime())): + oldestReceivedShare = receivedShare + } + + permission, err := cs3ReceivedShareToLibreGraphPermissions(ctx, logger, identityCache, resharing, receivedShare) + if err != nil { + return err + } + + // If at least one of the shares was accepted, we consider the driveItem's synchronized + // flag enabled. + // Also we use the Mountpoint name of the first accepted mountpoint as the name of + // of the driveItem + if receivedShare.GetState() == collaboration.ShareState_SHARE_STATE_ACCEPTED { + driveItem.SetClientSynchronize(true) + if name := receivedShare.GetMountPoint().GetPath(); name != "" && driveItem.GetName() == "" { + driveItem.SetName(receivedShare.GetMountPoint().GetPath()) + } + } + + // if at least one share is marked as hidden, consider the whole driveItem to be hidden + if receivedShare.GetHidden() { + driveItem.SetUIHidden(true) + } + + if userID := receivedShare.GetShare().GetCreator(); userID != nil { + identity, err := cs3UserIdToIdentity(ctx, identityCache, userID) + if err != nil { + logger.Warn().Err(err).Str("userid", userID.String()).Msg("could not get creator of the share") + } + + permission.SetInvitation( + libregraph.SharingInvitation{ + InvitedBy: &libregraph.IdentitySet{ + User: &identity, + }, + }, + ) + } + permissions = append(permissions, *permission) + + } + + // To stay compatible with the usershareprovider and the webdav + // service the id of the driveItem is composed of the StorageID and + // SpaceID of the sharestorage appended with the opaque ID of + // the oldest share for the resource: + // '$! + // Note: This means that the driveitem ID will change when the oldest + // shared is removed. It would be good to have are more stable ID here (e.g. + // derived from the shared resource's ID. But as we need to use the same + // ID across all services this means we needed to make similar adjustments + // to the sharejail (usershareprovider, webdav). Which we can't currently do + // as some clients rely on the IDs used there having a special format. + driveItem.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ + StorageId: utils.ShareStorageProviderID, + OpaqueId: oldestReceivedShare.GetShare().GetId().GetOpaqueId(), + SpaceId: utils.ShareStorageSpaceID, + })) + + if !driveItem.HasUIHidden() { + driveItem.SetUIHidden(false) + } + if !driveItem.HasClientSynchronize() { + driveItem.SetClientSynchronize(false) + if name := shareStat.GetInfo().GetName(); name != "" { + driveItem.SetName(name) + } + } + + remoteItem := libregraph.NewRemoteItem() + { + if id := shareStat.GetInfo().GetId(); id != nil { + remoteItem.SetId(storagespace.FormatResourceID(*id)) + } + + if name := shareStat.GetInfo().GetName(); name != "" { + remoteItem.SetName(name) + } + + if etag := shareStat.GetInfo().GetEtag(); etag != "" { + remoteItem.SetETag(etag) + } + + if mTime := shareStat.GetInfo().GetMtime(); mTime != nil { + remoteItem.SetLastModifiedDateTime(cs3TimestampToTime(mTime)) + } + + if size := shareStat.GetInfo().GetSize(); size != 0 { + remoteItem.SetSize(int64(size)) + } + + parentReference := libregraph.NewItemReference() + if spaceType := shareStat.GetInfo().GetSpace().GetSpaceType(); spaceType != "" { + parentReference.SetDriveType(spaceType) + } + + if root := shareStat.GetInfo().GetSpace().GetRoot(); root != nil { + parentReference.SetDriveId(storagespace.FormatResourceID(*root)) + } + if !reflect.ValueOf(*parentReference).IsZero() { + remoteItem.ParentReference = parentReference + } + + } + + // the parentReference of the outer driveItem should be the drive + // containing the mountpoint i.e. the share jail + driveItem.ParentReference = libregraph.NewItemReference() + driveItem.ParentReference.SetDriveType("virtual") + driveItem.ParentReference.SetDriveId(storagespace.FormatStorageID(utils.ShareStorageProviderID, utils.ShareStorageSpaceID)) + driveItem.ParentReference.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ + StorageId: utils.ShareStorageProviderID, + OpaqueId: utils.ShareStorageSpaceID, + SpaceId: utils.ShareStorageSpaceID, + })) + if etag := shareStat.GetInfo().GetEtag(); etag != "" { + driveItem.SetETag(etag) + } + + // connect the dots + { + if mTime := shareStat.GetInfo().GetMtime(); mTime != nil { + t := cs3TimestampToTime(mTime) + + driveItem.SetLastModifiedDateTime(t) + remoteItem.SetLastModifiedDateTime(t) + } + + if size := shareStat.GetInfo().GetSize(); size != 0 { + s := int64(size) + + driveItem.SetSize(s) + remoteItem.SetSize(s) + } + + if userID := shareStat.GetInfo().GetOwner(); userID != nil && userID.Type != cs3User.UserType_USER_TYPE_SPACE_OWNER { + identity, err := cs3UserIdToIdentity(ctx, identityCache, userID) + if err != nil { + // TODO: define a proper error behavior here. We don't + // want the whole request to fail just because a single + // resource owner couldn't be resolved. But, should be + // really return the affect share in the response? + // For now we just log a warning. The returned + // identitySet will just contain the userid. + logger.Warn().Err(err).Str("userid", userID.String()).Msg("could not get owner of shared resource") + } + + remoteItem.SetCreatedBy(libregraph.IdentitySet{User: &identity}) + driveItem.SetCreatedBy(libregraph.IdentitySet{User: &identity}) + } + switch info := shareStat.GetInfo(); { + case info.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER: + folder := libregraph.NewFolder() + + remoteItem.Folder = folder + driveItem.Folder = folder + case info.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_FILE: + file := libregraph.NewOpenGraphFile() + + if mimeType := info.GetMimeType(); mimeType != "" { + file.MimeType = &mimeType + } + + remoteItem.File = file + driveItem.File = file + } + + if len(permissions) > 0 { + remoteItem.Permissions = permissions + } + + if !reflect.ValueOf(*remoteItem).IsZero() { + driveItem.RemoteItem = remoteItem + } + } + + ch <- *driveItem + + return nil + }) + } + + var err error + // wait for concurrent requests to finish + go func() { + err = group.Wait() + close(ch) + }() + + driveItems := make([]libregraph.DriveItem, 0, len(receivedSharesByResourceID)) + for di := range ch { + driveItems = append(driveItems, di) + } + + return driveItems, err +} + +func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Logger, + identityCache identity.IdentityCache, resharing bool, receivedShare *collaboration.ReceivedShare) (*libregraph.Permission, error) { + permission := libregraph.NewPermission() + if id := receivedShare.GetShare().GetId().GetOpaqueId(); id != "" { + permission.SetId(id) + } + + if expiration := receivedShare.GetShare().GetExpiration(); expiration != nil { + permission.SetExpirationDateTime(cs3TimestampToTime(expiration)) + } + + if permissionSet := receivedShare.GetShare().GetPermissions().GetPermissions(); permissionSet != nil { + role := unifiedrole.CS3ResourcePermissionsToUnifiedRole( + *permissionSet, + unifiedrole.UnifiedRoleConditionGrantee, + resharing, + ) + + if role != nil { + permission.SetRoles([]string{role.GetId()}) + } + + actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*permissionSet) + + // actions only make sense if no role is set + if role == nil && len(actions) > 0 { + permission.SetLibreGraphPermissionsActions(actions) + } + } + switch grantee := receivedShare.GetShare().GetGrantee(); { + case grantee.GetType() == storageprovider.GranteeType_GRANTEE_TYPE_USER: + user, err := cs3UserIdToIdentity(ctx, identityCache, grantee.GetUserId()) + if err != nil { + logger.Error().Err(err).Msg("could not get user") + return nil, err + } + permission.SetGrantedToV2(libregraph.SharePointIdentitySet{User: &user}) + case grantee.GetType() == storageprovider.GranteeType_GRANTEE_TYPE_GROUP: + group, err := groupIdToIdentity(ctx, identityCache, grantee.GetGroupId().GetOpaqueId()) + if err != nil { + logger.Error().Err(err).Msg("could not get group") + return nil, err + } + permission.SetGrantedToV2(libregraph.SharePointIdentitySet{Group: &group}) + } + + return permission, nil +}