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 247ce0d1587..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,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 0894cf18b58..578c3d1cb05 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 +}