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.
This commit is contained in:
Ralf Haferkamp
2024-02-07 11:59:54 +01:00
committed by Ralf Haferkamp
parent 0a24f23164
commit 9d321bf379
7 changed files with 534 additions and 380 deletions
@@ -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.
@@ -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"))
})
})
})
+4 -4
View File
@@ -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{
+1 -1
View File
@@ -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
}
+4 -9
View File
@@ -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)
}
}
+1 -338
View File
@@ -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:
// '<sharestorageid>$<sharespaceid>!<share-opaque-id>
// 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())
}
+347 -1
View File
@@ -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:
// '<sharestorageid>$<sharespaceid>!<share-opaque-id>
// 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
}