From f689160b211bbe603a319b6603630fa10b63977d 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 | 64 +++- .../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 | 323 +---------------- services/graph/pkg/service/v0/utils.go | 332 +++++++++++++++++- 7 files changed, 517 insertions(+), 363 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 beab116bfd8..1c799eeecc4 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 + 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...) } - // fixMe: return a concrete driveItem - 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 24c90e7f810..9d5fa90e5bd 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 311c351afef..cad1abb6a9f 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 b1abfc73662..25c19b2dcec 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 0d308b847b9..340a0b0cf68 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 261de483c34..e23ca660bd4 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,318 +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 - resourceID := receivedShares[0].GetShare().GetResourceId() - shareStat, err := doStat(receivedShares[0].GetShare().GetResourceId()) - if shareStat == nil || err != nil { - return err - } - - driveItem := libregraph.NewDriveItem() - - // The id of the driveItem will be the composed of the StorageID and the SpaceID of the sharestorage - // appended with the ResourceID of the shared resource - // '$!::' - driveItem.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ - StorageId: utils.ShareStorageProviderID, - OpaqueId: resourceID.GetStorageId() + ":" + resourceID.GetSpaceId() + ":" + resourceID.GetOpaqueId(), - SpaceId: utils.ShareStorageSpaceID, - })) - permissions := make([]libregraph.Permission, 0, len(receivedShares)) - - for _, receivedShare := range receivedShares { - 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) - - } - - 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 0894cf18b58..161ba48aa39 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,325 @@ 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 + resourceID := receivedShares[0].GetShare().GetResourceId() + shareStat, err := doStat(receivedShares[0].GetShare().GetResourceId()) + if shareStat == nil || err != nil { + return err + } + + driveItem := libregraph.NewDriveItem() + + // The id of the driveItem will be the composed of the StorageID and the SpaceID of the sharestorage + // appended with the ResourceID of the shared resource + // '$!::' + driveItem.SetId(storagespace.FormatResourceID(storageprovider.ResourceId{ + StorageId: utils.ShareStorageProviderID, + OpaqueId: resourceID.GetStorageId() + ":" + resourceID.GetSpaceId() + ":" + resourceID.GetOpaqueId(), + SpaceId: utils.ShareStorageSpaceID, + })) + permissions := make([]libregraph.Permission, 0, len(receivedShares)) + + for _, receivedShare := range receivedShares { + 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) + + } + + 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 +}