From 56a3c215005b2e142032f4404e3dad48525bccab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 26 Apr 2024 17:22:24 +0200 Subject: [PATCH] rudimentary OCM support in graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../initial-ocm-support-for-graph.md | 5 + services/graph/mocks/base_graph_provider.go | 61 ++++ .../graph/mocks/drives_drive_item_provider.go | 61 ++++ services/graph/pkg/config/config.go | 9 +- services/graph/pkg/identity/backend.go | 6 + services/graph/pkg/identity/cache.go | 27 ++ .../service/v0/api_driveitem_permissions.go | 150 ++++++-- .../v0/api_driveitem_permissions_links.go | 15 +- .../pkg/service/v0/api_drives_drive_item.go | 32 +- .../service/v0/api_drives_drive_item_ocm.go | 173 ++++++++++ services/graph/pkg/service/v0/base.go | 14 +- services/graph/pkg/service/v0/sharedwithme.go | 22 +- services/graph/pkg/service/v0/users.go | 43 ++- services/graph/pkg/service/v0/utils.go | 320 +++++++++++++++++- 14 files changed, 872 insertions(+), 66 deletions(-) create mode 100644 changelog/unreleased/initial-ocm-support-for-graph.md create mode 100644 services/graph/pkg/service/v0/api_drives_drive_item_ocm.go diff --git a/changelog/unreleased/initial-ocm-support-for-graph.md b/changelog/unreleased/initial-ocm-support-for-graph.md new file mode 100644 index 00000000000..37e6f1f1f3b --- /dev/null +++ b/changelog/unreleased/initial-ocm-support-for-graph.md @@ -0,0 +1,5 @@ +Enhancement: Rudimentary OCM support in graph + +We now allow creating and accepting OCM shares. + +https://github.com/owncloud/ocis/pull/8909 diff --git a/services/graph/mocks/base_graph_provider.go b/services/graph/mocks/base_graph_provider.go index af846055d6e..538880cf747 100644 --- a/services/graph/mocks/base_graph_provider.go +++ b/services/graph/mocks/base_graph_provider.go @@ -10,6 +10,8 @@ import ( libregraph "github.com/owncloud/libre-graph-api-go" mock "github.com/stretchr/testify/mock" + + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" ) // BaseGraphProvider is an autogenerated mock type for the BaseGraphProvider type @@ -25,6 +27,65 @@ func (_m *BaseGraphProvider) EXPECT() *BaseGraphProvider_Expecter { return &BaseGraphProvider_Expecter{mock: &_m.Mock} } +// CS3ReceivedOCMSharesToDriveItems provides a mock function with given fields: ctx, receivedOCMShares +func (_m *BaseGraphProvider) CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error) { + ret := _m.Called(ctx, receivedOCMShares) + + if len(ret) == 0 { + panic("no return value specified for CS3ReceivedOCMSharesToDriveItems") + } + + var r0 []libregraph.DriveItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)); ok { + return rf(ctx, receivedOCMShares) + } + if rf, ok := ret.Get(0).(func(context.Context, []*ocmv1beta1.ReceivedShare) []libregraph.DriveItem); ok { + r0 = rf(ctx, receivedOCMShares) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]libregraph.DriveItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []*ocmv1beta1.ReceivedShare) error); ok { + r1 = rf(ctx, receivedOCMShares) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CS3ReceivedOCMSharesToDriveItems' +type BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call struct { + *mock.Call +} + +// CS3ReceivedOCMSharesToDriveItems is a helper method to define mock.On call +// - ctx context.Context +// - receivedOCMShares []*ocmv1beta1.ReceivedShare +func (_e *BaseGraphProvider_Expecter) CS3ReceivedOCMSharesToDriveItems(ctx interface{}, receivedOCMShares interface{}) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call { + return &BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call{Call: _e.mock.On("CS3ReceivedOCMSharesToDriveItems", ctx, receivedOCMShares)} +} + +func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) Run(run func(ctx context.Context, receivedOCMShares []*ocmv1beta1.ReceivedShare)) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]*ocmv1beta1.ReceivedShare)) + }) + return _c +} + +func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) Return(_a0 []libregraph.DriveItem, _a1 error) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) RunAndReturn(run func(context.Context, []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call { + _c.Call.Return(run) + return _c +} + // CS3ReceivedSharesToDriveItems provides a mock function with given fields: ctx, receivedShares func (_m *BaseGraphProvider) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error) { ret := _m.Called(ctx, receivedShares) diff --git a/services/graph/mocks/drives_drive_item_provider.go b/services/graph/mocks/drives_drive_item_provider.go index 6e8d4c9d619..77c0a3b767f 100644 --- a/services/graph/mocks/drives_drive_item_provider.go +++ b/services/graph/mocks/drives_drive_item_provider.go @@ -9,6 +9,8 @@ import ( mock "github.com/stretchr/testify/mock" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" @@ -146,6 +148,65 @@ func (_c *DrivesDriveItemProvider_GetSharesForResource_Call) RunAndReturn(run fu return _c } +// MountOCMShare provides a mock function with given fields: ctx, resourceID +func (_m *DrivesDriveItemProvider) MountOCMShare(ctx context.Context, resourceID *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error) { + ret := _m.Called(ctx, resourceID) + + if len(ret) == 0 { + panic("no return value specified for MountOCMShare") + } + + var r0 []*ocmv1beta1.ReceivedShare + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error)); ok { + return rf(ctx, resourceID) + } + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId) []*ocmv1beta1.ReceivedShare); ok { + r0 = rf(ctx, resourceID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*ocmv1beta1.ReceivedShare) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId) error); ok { + r1 = rf(ctx, resourceID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DrivesDriveItemProvider_MountOCMShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MountOCMShare' +type DrivesDriveItemProvider_MountOCMShare_Call struct { + *mock.Call +} + +// MountOCMShare is a helper method to define mock.On call +// - ctx context.Context +// - resourceID *providerv1beta1.ResourceId +func (_e *DrivesDriveItemProvider_Expecter) MountOCMShare(ctx interface{}, resourceID interface{}) *DrivesDriveItemProvider_MountOCMShare_Call { + return &DrivesDriveItemProvider_MountOCMShare_Call{Call: _e.mock.On("MountOCMShare", ctx, resourceID)} +} + +func (_c *DrivesDriveItemProvider_MountOCMShare_Call) Run(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId)) *DrivesDriveItemProvider_MountOCMShare_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId)) + }) + return _c +} + +func (_c *DrivesDriveItemProvider_MountOCMShare_Call) Return(_a0 []*ocmv1beta1.ReceivedShare, _a1 error) *DrivesDriveItemProvider_MountOCMShare_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DrivesDriveItemProvider_MountOCMShare_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_MountOCMShare_Call { + _c.Call.Return(run) + return _c +} + // MountShare provides a mock function with given fields: ctx, resourceID, name func (_m *DrivesDriveItemProvider) MountShare(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string) ([]*collaborationv1beta1.ReceivedShare, error) { ret := _m.Called(ctx, resourceID, name) diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 6d2bfb426da..b7339ca142d 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -25,10 +25,11 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` - Application Application `yaml:"application"` - Spaces Spaces `yaml:"spaces"` - Identity Identity `yaml:"identity"` - Events Events `yaml:"events"` + Application Application `yaml:"application"` + Spaces Spaces `yaml:"spaces"` + Identity Identity `yaml:"identity"` + IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"5.0"` + Events Events `yaml:"events"` Keycloak Keycloak `yaml:"keycloak"` ServiceAccount ServiceAccount `yaml:"service_account"` diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go index 99a38574cc8..8d8d25bb638 100644 --- a/services/graph/pkg/identity/backend.go +++ b/services/graph/pkg/identity/backend.go @@ -110,6 +110,12 @@ func CreateUserModelFromCS3(u *cs3user.User) *libregraph.User { u.Id = &cs3user.UserId{} } return &libregraph.User{ + Identities: []libregraph.ObjectIdentity{ + { + Issuer: &u.GetId().Idp, + IssuerAssignedId: &u.GetId().OpaqueId, + }, + }, DisplayName: &u.DisplayName, Mail: &u.Mail, OnPremisesSamAccountName: &u.Username, diff --git a/services/graph/pkg/identity/cache.go b/services/graph/pkg/identity/cache.go index a5670d13a30..2801e6a770d 100644 --- a/services/graph/pkg/identity/cache.go +++ b/services/graph/pkg/identity/cache.go @@ -110,6 +110,33 @@ func (cache IdentityCache) GetUser(ctx context.Context, userid string) (libregra return user, nil } +// GetAcceptedUser looks up a user by id, if the user is not cached, yet it will do a lookup via the CS3 API +func (cache IdentityCache) GetAcceptedUser(ctx context.Context, userid string) (libregraph.User, error) { + var user libregraph.User + if item := cache.users.Get(userid); item == nil { + gatewayClient, err := cache.gatewaySelector.Next() + if err != nil { + return libregraph.User{}, errorcode.New(errorcode.GeneralException, err.Error()) + } + cs3UserID := &cs3User.UserId{ + OpaqueId: userid, + } + u, err := revautils.GetAcceptedUserWithContext(ctx, cs3UserID, gatewayClient) + if err != nil { + if revautils.IsErrNotFound(err) { + return libregraph.User{}, ErrNotFound + } + return libregraph.User{}, errorcode.New(errorcode.GeneralException, err.Error()) + } + user = *CreateUserModelFromCS3(u) + cache.users.Set(userid, user, ttlcache.DefaultTTL) + + } else { + user = item.Value() + } + return user, nil +} + // GetGroup looks up a group by id, if the group is not cached, yet it will do a lookup via the CS3 API func (cache IdentityCache) GetGroup(ctx context.Context, groupID string) (libregraph.Group, error) { var group libregraph.Group diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index ee7916850d2..1ea3fe2c83a 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -2,15 +2,19 @@ package svc import ( "context" + "errors" "net/http" "net/url" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/publicshare" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/share" @@ -115,15 +119,6 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId stor objectID := driveRecipient.GetObjectId() cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(unifiedRolePermissions) - createShareRequest := &collaboration.CreateShareRequest{ - ResourceInfo: statResponse.GetInfo(), - Grant: &collaboration.ShareGrant{ - Permissions: &collaboration.SharePermissions{ - Permissions: cs3ResourcePermissions, - }, - }, - } - permission := &libregraph.Permission{} if role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*cs3ResourcePermissions, condition); role != nil { permission.Roles = []string{role.GetId()} @@ -133,6 +128,8 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId stor permission.LibreGraphPermissionsActions = unifiedrole.CS3ResourcePermissionsToLibregraphActions(*cs3ResourcePermissions) } + var shareid string + var expiration *typesv1beta1.Timestamp switch driveRecipient.GetLibreGraphRecipientType() { case "group": group, err := s.identityCache.GetGroup(ctx, objectID) @@ -140,64 +137,151 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId stor s.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup") return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error()) } - createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ - Type: storageprovider.GranteeType_GRANTEE_TYPE_GROUP, - Id: &storageprovider.Grantee_GroupId{GroupId: &grouppb.GroupId{ - OpaqueId: group.GetId(), - }}, - } permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ Group: &libregraph.Identity{ DisplayName: group.GetDisplayName(), Id: conversions.ToPointer(group.GetId()), }, } + createShareRequest := createShareRequestToGroup(group, statResponse.GetInfo(), cs3ResourcePermissions) + createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest) + if invite.ExpirationDateTime != nil { + createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime) + } + if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil { + s.logger.Debug().Err(err).Msg("share creation failed") + return libregraph.Permission{}, err + } + shareid = createShareResponse.GetShare().GetId().GetOpaqueId() + expiration = createShareResponse.GetShare().GetExpiration() default: + federated := false user, err := s.identityCache.GetUser(ctx, objectID) + if errors.Is(err, identity.ErrNotFound) && s.config.IncludeOCMSharees { + user, err = s.identityCache.GetAcceptedUser(ctx, objectID) + federated = true + } if err != nil { s.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup") return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error()) } - - createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ - Type: storageprovider.GranteeType_GRANTEE_TYPE_USER, - Id: &storageprovider.Grantee_UserId{UserId: &userpb.UserId{ - OpaqueId: user.GetId(), - }}, - } permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ User: &libregraph.Identity{ DisplayName: user.GetDisplayName(), Id: conversions.ToPointer(user.GetId()), }, } - } - if invite.ExpirationDateTime != nil { - createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime) - } + if federated { + if len(user.Identities) < 1 { + return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "user has no federated identity") + } + providerInfoResp, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + Domain: *user.Identities[0].Issuer, + }) + if err := errorcode.FromCS3Status(providerInfoResp.GetStatus(), err); err != nil { + s.logger.Error().Err(err).Msg("getting provider info failed") + return libregraph.Permission{}, err + } + + createShareRequest := createShareRequestToFederatedUser(user, statResponse.GetInfo().GetId(), providerInfoResp.ProviderInfo, cs3ResourcePermissions) + if invite.ExpirationDateTime != nil { + createShareRequest.Expiration = utils.TimeToTS(*invite.ExpirationDateTime) + } + createShareResponse, err := gatewayClient.CreateOCMShare(ctx, createShareRequest) + if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil { + s.logger.Error().Err(err).Msg("share creation failed") + return libregraph.Permission{}, err + } + shareid = createShareResponse.GetShare().GetId().GetOpaqueId() + expiration = createShareResponse.GetShare().GetExpiration() + } else { + createShareRequest := createShareRequestToUser(user, statResponse.GetInfo(), cs3ResourcePermissions) + if invite.ExpirationDateTime != nil { + createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime) + } + createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest) + if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil { + s.logger.Error().Err(err).Msg("share creation failed") + return libregraph.Permission{}, err + } + shareid = createShareResponse.GetShare().GetId().GetOpaqueId() + expiration = createShareResponse.GetShare().GetExpiration() + } - createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest) - if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil { - s.logger.Debug().Err(err).Msg("share creation failed") - return libregraph.Permission{}, err } - if id := createShareResponse.GetShare().GetId().GetOpaqueId(); id != "" { - permission.Id = conversions.ToPointer(id) + if shareid != "" { + permission.Id = conversions.ToPointer(shareid) } else if IsSpaceRoot(statResponse.GetInfo().GetId()) { // permissions on a space root are not handled by a share manager so // they don't get a share-id permission.SetId(identitySetToSpacePermissionID(permission.GetGrantedToV2())) } - if expiration := createShareResponse.GetShare().GetExpiration(); expiration != nil { + if expiration != nil { permission.SetExpirationDateTime(utils.TSToTime(expiration)) } return *permission, nil } +func createShareRequestToGroup(group libregraph.Group, info *storageprovider.ResourceInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *collaboration.CreateShareRequest { + return &collaboration.CreateShareRequest{ + ResourceInfo: info, + Grant: &collaboration.ShareGrant{ + Grantee: &storageprovider.Grantee{ + Type: storageprovider.GranteeType_GRANTEE_TYPE_GROUP, + Id: &storageprovider.Grantee_GroupId{GroupId: &grouppb.GroupId{ + OpaqueId: group.GetId(), + }}, + }, + Permissions: &collaboration.SharePermissions{ + Permissions: cs3ResourcePermissions, + }, + }, + } +} +func createShareRequestToUser(user libregraph.User, info *storageprovider.ResourceInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *collaboration.CreateShareRequest { + return &collaboration.CreateShareRequest{ + ResourceInfo: info, + Grant: &collaboration.ShareGrant{ + Grantee: &storageprovider.Grantee{ + Type: storageprovider.GranteeType_GRANTEE_TYPE_USER, + Id: &storageprovider.Grantee_UserId{UserId: &userpb.UserId{ + OpaqueId: user.GetId(), + }}, + }, + Permissions: &collaboration.SharePermissions{ + Permissions: cs3ResourcePermissions, + }, + }, + } +} +func createShareRequestToFederatedUser(user libregraph.User, resourceId *storageprovider.ResourceId, providerInfo *ocmprovider.ProviderInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *ocm.CreateOCMShareRequest { + return &ocm.CreateOCMShareRequest{ + ResourceId: resourceId, + Grantee: &storageprovider.Grantee{ + Type: storageprovider.GranteeType_GRANTEE_TYPE_USER, + Id: &storageprovider.Grantee_UserId{UserId: &userpb.UserId{ + Type: userpb.UserType_USER_TYPE_FEDERATED, + OpaqueId: user.GetId(), + Idp: providerInfo.Domain, // the domain is persisted in the grant as u:{opaqueid}:{domain} + }}, + }, + RecipientMeshProvider: providerInfo, + AccessMethods: []*ocm.AccessMethod{ + { + Term: &ocm.AccessMethod_WebdavOptions{ + WebdavOptions: &ocm.WebDAVAccessMethod{ + Permissions: cs3ResourcePermissions, + }, + }, + }, + }, + } +} + // SpaceRootInvite handles invitation request on project spaces func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { gatewayClient, err := s.gatewaySelector.Next() diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_links.go b/services/graph/pkg/service/v0/api_driveitem_permissions_links.go index 3de834a331d..dcace36a744 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_links.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_links.go @@ -9,7 +9,6 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" - providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" @@ -17,7 +16,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" - "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" "github.com/owncloud/ocis/v2/services/graph/pkg/linktype" ) @@ -66,14 +64,14 @@ func (s DriveItemPermissionsService) CreateLink(ctx context.Context, driveItemID if isSet { expireTime := parseAndFillUpTime(expirationDate) if expireTime == nil { - s.logger.Debug().Interface("createLink", createLink).Msg(err.Error()) + s.logger.Debug().Interface("createLink", createLink).Send() return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid expiration date") } req.GetGrant().Expiration = expireTime } // set displayname and password protected as arbitrary metadata - req.ResourceInfo.ArbitraryMetadata = &providerv1beta1.ArbitraryMetadata{ + req.ResourceInfo.ArbitraryMetadata = &storageprovider.ArbitraryMetadata{ Metadata: map[string]string{ "name": createLink.GetDisplayName(), "quicklink": strconv.FormatBool(createLink.GetLibreGraphQuickLink()), @@ -274,7 +272,7 @@ func (api DriveItemPermissionsApi) SetSpaceRootLinkPassword(w http.ResponseWrite render.JSON(w, r, newPermission) } -func (s DriveItemPermissionsService) updatePublicLinkPermission(ctx context.Context, permissionID string, itemID *providerv1beta1.ResourceId, newPermission *libregraph.Permission) (perm *libregraph.Permission, err error) { +func (s DriveItemPermissionsService) updatePublicLinkPermission(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId, newPermission *libregraph.Permission) (perm *libregraph.Permission, err error) { gatewayClient, err := s.gatewaySelector.Next() if err != nil { s.logger.Error().Err(err).Msg("could not select next gateway client") @@ -283,8 +281,8 @@ func (s DriveItemPermissionsService) updatePublicLinkPermission(ctx context.Cont statResp, err := gatewayClient.Stat( ctx, - &providerv1beta1.StatRequest{ - Ref: &providerv1beta1.Reference{ + &storageprovider.StatRequest{ + Ref: &storageprovider.Reference{ ResourceId: itemID, Path: ".", }, @@ -326,6 +324,9 @@ func (s DriveItemPermissionsService) updatePublicLinkPermission(ctx context.Cont }, statResp.GetInfo().GetType(), ) + if err != nil { + return nil, err + } update := &link.UpdatePublicShareRequest_Update{ Type: link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS, Grant: &link.Grant{ 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 962d427769c..b51e9f2e1d5 100644 --- a/services/graph/pkg/service/v0/api_drives_drive_item.go +++ b/services/graph/pkg/service/v0/api_drives_drive_item.go @@ -8,6 +8,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" @@ -15,6 +16,7 @@ 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" @@ -81,6 +83,9 @@ type ( // MountShare mounts a share MountShare(ctx context.Context, resourceID *storageprovider.ResourceId, name string) ([]*collaboration.ReceivedShare, error) + // MountOCMShare mounts an OCM share + MountOCMShare(ctx context.Context, resourceID *storageprovider.ResourceId /*, name string*/) ([]*ocm.ReceivedShare, error) + // UnmountShare unmounts a share UnmountShare(ctx context.Context, shareID *collaboration.ShareId) error @@ -516,14 +521,29 @@ func (api DrivesDriveItemApi) CreateDriveItem(w http.ResponseWriter, r *http.Req return } - mountedShares, err := api.drivesDriveItemService.MountShare(ctx, &resourceId, requestDriveItem.GetName()) - if err != nil { - api.logger.Debug().Err(err).Msg(err.Error()) - errorcode.RenderError(w, r, err) - return + var driveItems []libregraph.DriveItem + switch { + case resourceId.GetStorageId() == utils.OCMStorageProviderID: + var mountedOcmShares []*ocm.ReceivedShare + mountedOcmShares, err = api.drivesDriveItemService.MountOCMShare(ctx, &resourceId /*, requestDriveItem.GetName()*/) + if err != nil { + api.logger.Debug().Err(err).Msg(ErrMountShare.Error()) + errorcode.RenderError(w, r, err) + return + } + driveItems, err = api.baseGraphService.CS3ReceivedOCMSharesToDriveItems(ctx, mountedOcmShares) + default: + var mountedShares []*collaboration.ReceivedShare + // Get all shares that the user has received for this resource. There might be multiple + mountedShares, err = api.drivesDriveItemService.MountShare(ctx, &resourceId, requestDriveItem.GetName()) + if err != nil { + api.logger.Debug().Err(err).Msg(err.Error()) + errorcode.RenderError(w, r, err) + return + } + driveItems, err = api.baseGraphService.CS3ReceivedSharesToDriveItems(ctx, mountedShares) } - driveItems, err := api.baseGraphService.CS3ReceivedSharesToDriveItems(ctx, mountedShares) switch { case err != nil: break diff --git a/services/graph/pkg/service/v0/api_drives_drive_item_ocm.go b/services/graph/pkg/service/v0/api_drives_drive_item_ocm.go new file mode 100644 index 00000000000..2b3751fbd88 --- /dev/null +++ b/services/graph/pkg/service/v0/api_drives_drive_item_ocm.go @@ -0,0 +1,173 @@ +package svc + +import ( + "context" + "errors" + + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" +) + +var ( + // ErrUnmountOCMShare is returned when unmounting a share fails + ErrUnmountOCMShare = errorcode.New(errorcode.InvalidRequest, "unmounting ocm share failed") + + // ErrMountOCMShare is returned when mounting a share fails + ErrMountOCMShare = errorcode.New(errorcode.InvalidRequest, "mounting ocm share failed") +) + +type ( + // UpdateOCMShareClosure is a closure that injects required updates into the update request + UpdateOCMShareClosure func(share *ocm.ReceivedShare, request *ocm.UpdateReceivedOCMShareRequest) +) + +// GetOCMSharesForResource returns all federated shares for a given resourceID +func (s DrivesDriveItemService) GetOCMSharesForResource(ctx context.Context, resourceID *storageprovider.ResourceId) ([]*ocm.ReceivedShare, error) { + // Find all accepted shares for this resource + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return nil, err + } + + receivedOCMSharesResponse, err := gatewayClient.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{ + /* ocm has no filters, yet + Filters: append([]*collaboration.Filter{ + { + Type: collaboration.Filter_TYPE_RESOURCE_ID, + Term: &collaboration.Filter_ResourceId{ + ResourceId: resourceID, + }, + }, + }, filters...), + */ + }) + switch { + case err != nil: + return nil, err + case len(receivedOCMSharesResponse.GetShares()) == 0: + return nil, ErrNoShares + default: + return receivedOCMSharesResponse.GetShares(), errorcode.FromCS3Status(receivedOCMSharesResponse.GetStatus(), err) + } +} + +// UpdateShares updates multiple shares; +// it could happen that some shares are updated and some are not, +// this will return a list of updated shares and a list of errors; +// there is no guarantee that all updates are successful +func (s DrivesDriveItemService) UpdateOCMShares(ctx context.Context, shares []*ocm.ReceivedShare, updater UpdateOCMShareClosure) ([]*ocm.ReceivedShare, error) { + errs := make([]error, 0, len(shares)) + updatedShares := make([]*ocm.ReceivedShare, 0, len(shares)) + + for _, share := range shares { + err := s.UpdateOCMShare( + ctx, + share, + updater, + ) + if err != nil { + errs = append(errs, err) + continue + } + + updatedShares = append(updatedShares, share) + } + + return updatedShares, errors.Join(errs...) +} + +// UpdateOCMShare updates a single share +func (s DrivesDriveItemService) UpdateOCMShare(ctx context.Context, share *ocm.ReceivedShare, updater UpdateOCMShareClosure) error { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return err + } + + updateReceivedOCMShareRequest := &ocm.UpdateReceivedOCMShareRequest{ + Share: &ocm.ReceivedShare{ + Id: share.GetId(), + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + } + + switch updater { + case nil: + return ErrNoUpdater + default: + updater(share, updateReceivedOCMShareRequest) + } + + if len(updateReceivedOCMShareRequest.GetUpdateMask().GetPaths()) == 0 { + return ErrNoUpdates + } + + updateReceivedOCMShareResponse, err := gatewayClient.UpdateReceivedOCMShare(ctx, updateReceivedOCMShareRequest) + return errorcode.FromCS3Status(updateReceivedOCMShareResponse.GetStatus(), err) +} + +func (s DrivesDriveItemService) MountOCMShare(ctx context.Context, resourceID *storageprovider.ResourceId /*, name string*/) ([]*ocm.ReceivedShare, error) { + /* + if filepath.IsAbs(name) { + return nil, ErrAbsoluteNamePath + } + + if name != "" { + name = filepath.Clean(name) + } + */ + + shares, err := s.GetOCMSharesForResource(ctx, resourceID) + if err != nil { + return nil, err + } + + availableShares := make([]*ocm.ReceivedShare, 0, len(shares)) + mountedShares := make([]*ocm.ReceivedShare, 0, 1) + for _, v := range shares { + switch v.GetState() { + case ocm.ShareState_SHARE_STATE_ACCEPTED: + mountedShares = append(mountedShares, v) + case ocm.ShareState_SHARE_STATE_PENDING, ocm.ShareState_SHARE_STATE_REJECTED: + availableShares = append(availableShares, v) + } + } + if len(availableShares) == 0 { + if len(mountedShares) > 0 { + return nil, ErrAlreadyMounted + } + return nil, ErrNoShares + } + + updatedShares, err := s.UpdateOCMShares(ctx, availableShares, func(share *ocm.ReceivedShare, request *ocm.UpdateReceivedOCMShareRequest) { + request.Share.State = ocm.ShareState_SHARE_STATE_ACCEPTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState) + + // only update if mountPoint name is not empty and the path has changed + /* ocm shares have no mount point??? + if name != "" { + mountPoint := share.GetMountPoint() + if mountPoint == nil { + mountPoint = &storageprovider.Reference{} + } + + if filepath.Clean(mountPoint.GetPath()) != name { + mountPoint.Path = name + request.Share.MountPoint = mountPoint + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathMountPoint) + } + } + */ + }) + + errs, ok := err.(interface{ Unwrap() []error }) + if ok && len(errs.Unwrap()) == len(availableShares) { + // none of the received ocm shares could be accepted. + // this is an error, return it. + return nil, err + } + + return updatedShares, nil +} diff --git a/services/graph/pkg/service/v0/base.go b/services/graph/pkg/service/v0/base.go index f78a182c9df..285595facdf 100644 --- a/services/graph/pkg/service/v0/base.go +++ b/services/graph/pkg/service/v0/base.go @@ -10,10 +10,10 @@ import ( "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" libregraph "github.com/owncloud/libre-graph-api-go" @@ -35,6 +35,7 @@ import ( // BaseGraphProvider is the interface that wraps shared methods between the different graph providers type BaseGraphProvider interface { CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) + CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error) } // BaseGraphService implements a couple of helper functions that are @@ -71,7 +72,7 @@ func (g BaseGraphService) getDriveItem(ctx context.Context, ref storageprovider. if err != nil { return nil, err } - if res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK { + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { refStr, _ := storagespace.FormatReference(&ref) return nil, fmt.Errorf("could not stat %s: %s", refStr, res.GetStatus().GetMessage()) } @@ -87,6 +88,15 @@ func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, rec return cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, receivedShares) } +func (g BaseGraphService) CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + return nil, err + } + + return cs3ReceivedOCMSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, receivedShares) +} + func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, space *storageprovider.StorageSpace, apiVersion APIVersion) []libregraph.Permission { if space.Opaque == nil { return nil diff --git a/services/graph/pkg/service/v0/sharedwithme.go b/services/graph/pkg/service/v0/sharedwithme.go index 8d88206d272..d369e508205 100644 --- a/services/graph/pkg/service/v0/sharedwithme.go +++ b/services/graph/pkg/service/v0/sharedwithme.go @@ -5,6 +5,7 @@ import ( "net/http" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" @@ -38,6 +39,25 @@ func (g Graph) listSharedWithMe(ctx context.Context) ([]libregraph.DriveItem, er g.logger.Error().Err(err).Msg("listing shares failed") return nil, err } + driveItems, err := cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, listReceivedSharesResponse.GetShares()) + if err != nil { + g.logger.Error().Err(err).Msg("could not convert received shares to drive items") + return nil, err + } + + if g.config.IncludeOCMSharees { + listReceivedOCMSharesResponse, err := gatewayClient.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{}) + if err := errorcode.FromCS3Status(listReceivedSharesResponse.GetStatus(), err); err != nil { + g.logger.Error().Err(err).Msg("listing shares failed") + return nil, err + } + ocmDriveItems, err := cs3ReceivedOCMSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, listReceivedOCMSharesResponse.GetShares()) + if err != nil { + g.logger.Error().Err(err).Msg("could not convert received shares to drive items") + return nil, err + } + driveItems = append(driveItems, ocmDriveItems...) + } - return cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, listReceivedSharesResponse.GetShares()) + return driveItems, err } diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 258dc847d0f..7f8f9b39a61 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -13,11 +13,8 @@ import ( "strconv" "strings" - "github.com/google/uuid" - - "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" - "github.com/CiscoM31/godata" + invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" @@ -26,14 +23,14 @@ import ( "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" "github.com/go-chi/render" + "github.com/google/uuid" libregraph "github.com/owncloud/libre-graph-api-go" - settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" - settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" "github.com/owncloud/ocis/v2/services/graph/pkg/identity" ocissettingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults" ) // GetMe implements the Service interface. @@ -106,7 +103,7 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { } func (g Graph) fetchAppRoleAssignments(ctx context.Context, accountuuid string) ([]libregraph.AppRoleAssignment, error) { - lrar, err := g.roleService.ListRoleAssignments(ctx, &settings.ListRoleAssignmentsRequest{ + lrar, err := g.roleService.ListRoleAssignments(ctx, &settingssvc.ListRoleAssignmentsRequest{ AccountUuid: accountuuid, }) if err != nil { @@ -257,6 +254,34 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { users, err = g.identityBackend.GetUsers(r.Context(), odataReq) } + if g.config.IncludeOCMSharees { + gwc, err := g.gatewaySelector.Next() + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + term, err := identity.GetSearchValues(odataReq.Query) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + remoteUsersRes, err := gwc.FindAcceptedUsers(r.Context(), &invitepb.FindAcceptedUsersRequest{Filter: term}) + if err != nil { + // TODO grpc FindAcceptedUsers call failed + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + if remoteUsersRes.Status.Code != cs3rpc.Code_CODE_OK { + // TODO "error searching remote users" + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, remoteUsersRes.Status.Message) + return + } + for _, user := range remoteUsersRes.GetAcceptedUsers() { + users = append(users, identity.CreateUserModelFromCS3(user)) + } + } + if err != nil { logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users from backend") var errcode errorcode.Error @@ -390,7 +415,7 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { if g.roleService != nil && g.config.API.AssignDefaultUserRole { // All users get the user role by default currently. // to all new users for now, as create Account request does not have any role field - if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{ + if _, err = g.roleService.AssignRoleToUser(r.Context(), &settingssvc.AssignRoleToUserRequest{ AccountUuid: *u.Id, RoleId: ocissettingssvc.BundleUUIDRoleUser, }); err != nil { @@ -815,7 +840,7 @@ func (g Graph) patchUser(w http.ResponseWriter, r *http.Request, nameOrID string } vID = tvID.String() } - _, err = g.valueService.SaveValue(r.Context(), &settings.SaveValueRequest{ + _, err = g.valueService.SaveValue(r.Context(), &settingssvc.SaveValueRequest{ Value: &settingsmsg.Value{ Id: vID, BundleId: defaults.BundleUUIDProfile, diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index 3daf87b2a17..4ed9c686b9f 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -11,18 +11,16 @@ import ( 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" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - "golang.org/x/sync/errgroup" - "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" - 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" + "golang.org/x/sync/errgroup" ) // StrictJSONUnmarshal is a wrapper around json.Unmarshal that returns an error if the json contains unknown fields. @@ -472,3 +470,317 @@ func ExtractShareIdFromResourceId(rid storageprovider.ResourceId) *collaboration OpaqueId: rid.GetOpaqueId(), } } + +func cs3ReceivedOCMSharesToDriveItems(ctx context.Context, + logger *log.Logger, + gatewayClient gateway.GatewayAPIClient, + identityCache identity.IdentityCache, + receivedShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error) { + + ch := make(chan libregraph.DriveItem) + group := new(errgroup.Group) + // Set max concurrency + group.SetLimit(10) + + receivedSharesByResourceID := make(map[string][]*ocm.ReceivedShare, len(receivedShares)) + for _, receivedShare := range receivedShares { + rIDStr := receivedShare.GetRemoteShareId() + receivedSharesByResourceID[rIDStr] = append(receivedSharesByResourceID[rIDStr], receivedShare) + } + + for _, receivedSharesForResource := range receivedSharesByResourceID { + receivedShares := receivedSharesForResource + + group.Go(func() error { + var err error // redeclare + shareStat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{ + Ref: &storageprovider.Reference{ + ResourceId: &storageprovider.ResourceId{ + // TODO maybe the reference is wrong + StorageId: utils.OCMStorageProviderID, + SpaceId: receivedShares[0].GetId().GetOpaqueId(), + OpaqueId: "", // in OCM resources the opaque id is the base64 encoded path + //OpaqueId: maybe ? receivedShares[0].GetId().GetOpaqueId(), + }, + }, + }) + + var errCode errorcode.Error + errors.As(errorcode.FromCS3Status(shareStat.GetStatus(), err), &errCode) + + switch { + // skip ItemNotFound shares, they might have been deleted in the meantime or orphans. + case errCode.GetCode() == errorcode.ItemNotFound: + return nil + case err == nil: + break + default: + logger.Error().Err(errCode).Msg("could not stat") + return errCode + } + + driveItem, err := fillDriveItemPropertiesFromReceivedOCMShare(ctx, logger, identityCache, receivedShares, shareStat.GetInfo()) + if err != nil { + return err + } + + if !driveItem.HasUIHidden() { + driveItem.SetUIHidden(false) + } + if !driveItem.HasClientSynchronize() { + driveItem.SetClientSynchronize(false) + if name := shareStat.GetInfo().GetName(); name != "" { + driveItem.SetName(name) // FIXME name is not set??? + } + } + + remoteItem := driveItem.RemoteItem + { + 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 we + // really return the affected 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 + } + } + + 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 fillDriveItemPropertiesFromReceivedOCMShare(ctx context.Context, logger *log.Logger, + identityCache identity.IdentityCache, receivedShares []*ocm.ReceivedShare, + resourceInfo *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { + + driveItem := libregraph.NewDriveItem() + permissions := make([]libregraph.Permission, 0, len(receivedShares)) + + var oldestReceivedShare *ocm.ReceivedShare + for _, receivedShare := range receivedShares { + switch { + case oldestReceivedShare == nil: + fallthrough + case utils.TSToTime(receivedShare.GetCtime()).Before(utils.TSToTime(oldestReceivedShare.GetCtime())): + oldestReceivedShare = receivedShare + } + + permission, err := cs3ReceivedOCMShareToLibreGraphPermissions(ctx, logger, identityCache, receivedShare, resourceInfo) + if err != nil { + return driveItem, err + } + + driveItem.SetName(resourceInfo.GetName()) + + // 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 for + // the driveItem + if receivedShare.GetState() == ocm.ShareState_SHARE_STATE_ACCEPTED { + driveItem.SetClientSynchronize(true) + if name := receivedShare.GetName(); name != "" && driveItem.GetName() == "" { + driveItem.SetName(receivedShare.GetName()) + } + } + + // 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.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 ocm 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 + // share 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.OCMStorageProviderID, + SpaceId: utils.OCMStorageSpaceID, + OpaqueId: oldestReceivedShare.GetRemoteShareId(), + })) + + } + driveItem.RemoteItem = libregraph.NewRemoteItem() + driveItem.RemoteItem.Permissions = permissions + return driveItem, nil +} + +func cs3ReceivedOCMShareToLibreGraphPermissions(ctx context.Context, logger *log.Logger, + identityCache identity.IdentityCache, receivedShare *ocm.ReceivedShare, + _ *storageprovider.ResourceInfo) (*libregraph.Permission, error) { + permission := libregraph.NewPermission() + if id := receivedShare.GetId().GetOpaqueId(); id != "" { + permission.SetId(id) + } + + if expiration := receivedShare.GetExpiration(); expiration != nil { + permission.SetExpirationDateTime(cs3TimestampToTime(expiration)) + } + + /* + if permissionSet := receivedShare.GetShare().GetPermissions().GetPermissions(); permissionSet != nil { + condition, err := roleConditionForResourceType(resourceInfo) + if err != nil { + return nil, err + } + role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*permissionSet, condition) + + 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.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 +}