Files
opencloud/services/graph/pkg/service/v0/base.go
Christian Richter 28fdfded71 incorporate requested changes
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-12 17:50:28 +01:00

1323 lines
41 KiB
Go

package svc
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/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/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/cache"
"github.com/opencloud-eu/opencloud/services/graph/pkg/linktype"
"github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole"
)
// 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
// shared between the different graph services
type BaseGraphService struct {
logger *log.Logger
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
identityCache cache.IdentityCache
config *config.Config
availableRoles []*libregraph.UnifiedRoleDefinition
}
func (g BaseGraphService) getSpaceRootPermissions(ctx context.Context, spaceID *storageprovider.StorageSpaceId, countOnly bool) ([]libregraph.Permission, int, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, 0, err
}
space, err := utils.GetSpace(ctx, spaceID.GetOpaqueId(), gatewayClient)
if err != nil {
return nil, 0, errorcode.FromUtilsStatusCodeError(err)
}
perm, count := g.cs3SpacePermissionsToLibreGraph(ctx, space, countOnly, APIVersion_1_Beta_1)
return perm, count, nil
}
func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
return nil, err
}
res, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: ref})
if err != nil {
return nil, err
}
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())
}
return cs3ResourceToDriveItem(g.logger, res.GetInfo())
}
func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
return nil, err
}
return cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, receivedShares, g.availableRoles)
}
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, g.availableRoles)
}
func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, space *storageprovider.StorageSpace, countOnly bool, apiVersion APIVersion) ([]libregraph.Permission, int) {
if space.Opaque == nil {
return nil, 0
}
logger := g.logger.SubloggerWithRequestID(ctx)
var permissionsMap map[string]*storageprovider.ResourcePermissions
opaqueGrants, ok := space.Opaque.Map["grants"]
if ok {
err := json.Unmarshal(opaqueGrants.Value, &permissionsMap)
if err != nil {
logger.Debug().
Err(err).
Interface("space", space.Root).
Bytes("grants", opaqueGrants.Value).
Msg("unable to parse space: failed to read spaces grants")
}
}
if len(permissionsMap) == 0 {
return nil, 0
}
if countOnly {
// If we only need the count, we can return early
return nil, len(permissionsMap)
}
var permissionsExpirations map[string]*types.Timestamp
opaqueGrantsExpirations, ok := space.Opaque.Map["grants_expirations"]
if ok {
err := json.Unmarshal(opaqueGrantsExpirations.Value, &permissionsExpirations)
if err != nil {
logger.Debug().
Err(err).
Interface("space", space.Root).
Bytes("grants_expirations", opaqueGrantsExpirations.Value).
Msg("unable to parse space: failed to read spaces grants expirations")
}
}
var groupsMap map[string]struct{}
opaqueGroups, ok := space.Opaque.Map["groups"]
if ok {
err := json.Unmarshal(opaqueGroups.Value, &groupsMap)
if err != nil {
logger.Debug().
Err(err).
Interface("space", space.Root).
Bytes("groups", opaqueGroups.Value).
Msg("unable to parse space: failed to read spaces groups")
}
}
permissions := make([]libregraph.Permission, 0, len(permissionsMap))
for id, perm := range permissionsMap {
// This temporary variable is necessary since we need to pass a pointer to the
// libregraph.Identity and if we pass the pointer from the loop every identity
// will have the same id.
tmp := id
isGroup := false
var cs3Identity libregraph.Identity
var err error
var p libregraph.Permission
if _, ok := groupsMap[id]; ok {
cs3Identity, err = groupIdToIdentity(ctx, g.identityCache, tmp)
if err != nil {
g.logger.Warn().Str("groupid", tmp).Msg("Group not found by id")
}
isGroup = true
} else {
tenantId := revactx.ContextMustGetUser(ctx).GetId().GetTenantId()
cs3Identity, err = userIdToIdentity(ctx, g.identityCache, tenantId, tmp)
if err != nil {
g.logger.Warn().Str("userid", tmp).Msg("User not found by id")
}
}
switch apiVersion {
case APIVersion_1:
var identitySet libregraph.IdentitySet
if isGroup {
identitySet.SetGroup(cs3Identity)
} else {
identitySet.SetUser(cs3Identity)
}
p.SetGrantedToV2(libregraph.SharePointIdentitySet{User: identitySet.User, Group: identitySet.Group})
// FIXME: needs to be removed
p.SetGrantedToIdentities([]libregraph.IdentitySet{identitySet})
case APIVersion_1_Beta_1:
var identitySet libregraph.SharePointIdentitySet
if isGroup {
identitySet.SetGroup(cs3Identity)
} else {
identitySet.SetUser(cs3Identity)
}
p.SetId(identitySetToSpacePermissionID(identitySet))
p.SetGrantedToV2(identitySet)
}
if exp := permissionsExpirations[id]; exp != nil {
p.SetExpirationDateTime(time.Unix(int64(exp.GetSeconds()), int64(exp.GetNanos())))
}
if role := unifiedrole.CS3ResourcePermissionsToRole(
g.availableRoles,
perm,
unifiedrole.UnifiedRoleConditionDrive,
false,
); role != nil {
switch apiVersion {
case APIVersion_1:
if r := unifiedrole.GetLegacyRoleName(*role); r != "" {
p.SetRoles([]string{r})
}
case APIVersion_1_Beta_1:
p.SetRoles([]string{role.GetId()})
}
}
// if there is no role, we need to set the actions as a fallback
// this could happen if a role is disabled or unknown
if !p.HasRoles() {
p.SetLibreGraphPermissionsActions(unifiedrole.CS3ResourcePermissionsToLibregraphActions(perm))
}
permissions = append(permissions, p)
}
return permissions, len(permissions)
}
func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) {
webURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
g.logger.Error().
Err(err).
Str("url", g.config.Spaces.WebDavBase).
Msg("failed to parse webURL base url")
return nil, err
}
lt, actions := linktype.SharingLinkTypeFromCS3Permissions(createdLink.GetPermissions())
perm := libregraph.NewPermission()
perm.Id = libregraph.PtrString(createdLink.GetId().GetOpaqueId())
perm.Link = &libregraph.SharingLink{
Type: lt,
PreventsDownload: libregraph.PtrBool(false),
LibreGraphDisplayName: libregraph.PtrString(createdLink.GetDisplayName()),
LibreGraphQuickLink: libregraph.PtrBool(createdLink.GetQuicklink()),
}
perm.LibreGraphPermissionsActions = actions
webURL.Path = path.Join(webURL.Path, "s", createdLink.GetToken())
perm.Link.SetWebUrl(webURL.String())
// set expiration date
if createdLink.GetExpiration() != nil {
perm.SetExpirationDateTime(cs3TimestampToTime(createdLink.GetExpiration()).UTC())
}
// set cTime
if createdLink.GetCtime() != nil {
perm.SetCreatedDateTime(cs3TimestampToTime(createdLink.GetCtime()).UTC())
}
perm.SetHasPassword(createdLink.GetPasswordProtected())
return perm, nil
}
func (g BaseGraphService) listUserShares(ctx context.Context, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
concreteFilters := []*collaboration.Filter{
share.UserGranteeFilter(),
share.GroupGranteeFilter(),
}
concreteFilters = append(concreteFilters, filters...)
lsUserSharesRequest := collaboration.ListSharesRequest{
Filters: concreteFilters,
}
lsUserSharesResponse, err := gatewayClient.ListShares(ctx, &lsUserSharesRequest)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsUserSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsUserSharesResponse.Status.Message)
}
driveItems, err = g.cs3UserSharesToDriveItems(ctx, lsUserSharesResponse.Shares, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g BaseGraphService) listOCMShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
concreteFilters := []*ocm.ListOCMSharesRequest_Filter{}
concreteFilters = append(concreteFilters, filters...)
lsOCMSharesRequest := ocm.ListOCMSharesRequest{
Filters: concreteFilters,
}
lsOCMSharesResponse, err := gatewayClient.ListOCMShares(ctx, &lsOCMSharesRequest)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsOCMSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsOCMSharesResponse.Status.Message)
}
driveItems, err = g.cs3OCMSharesToDriveItems(ctx, lsOCMSharesResponse.Shares, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g BaseGraphService) listPublicShares(ctx context.Context, filters []*link.ListPublicSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
var concreteFilters []*link.ListPublicSharesRequest_Filter
concreteFilters = append(concreteFilters, filters...)
req := link.ListPublicSharesRequest{
Filters: concreteFilters,
}
lsPublicSharesResponse, err := gatewayClient.ListPublicShares(ctx, &req)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsPublicSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsPublicSharesResponse.Status.Message)
}
driveItems, err = g.cs3PublicSharesToDriveItems(ctx, lsPublicSharesResponse.Share, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g BaseGraphService) cs3UserSharesToDriveItems(ctx context.Context, shares []*collaboration.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
errg, ctx := errgroup.WithContext(ctx)
// group shares by resource id
sharesByResource := make(map[string][]*collaboration.Share)
for _, share := range shares {
sharesByResource[share.GetResourceId().String()] = append(sharesByResource[share.GetResourceId().String()], share)
}
type resourceShares struct {
ResourceID *storageprovider.ResourceId
Shares []*collaboration.Share
}
work := make(chan resourceShares, len(shares))
results := make(chan *libregraph.DriveItem, len(shares))
// Distribute work
errg.Go(func() error {
defer close(work)
for _, shares := range sharesByResource {
select {
case work <- resourceShares{ResourceID: shares[0].GetResourceId(), Shares: shares}:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
// Spawn workers that'll concurrently work the queue
numWorkers := g.config.MaxConcurrency
if len(sharesByResource) < numWorkers {
numWorkers = len(sharesByResource)
}
for i := 0; i < numWorkers; i++ {
errg.Go(func() error {
for sharesByResource := range work {
resIDStr := storagespace.FormatResourceID(sharesByResource.ResourceID)
// check if we already have the drive item in the map
item, ok := driveItems[resIDStr]
if !ok {
itemptr, err := g.getDriveItem(ctx, &storageprovider.Reference{ResourceId: sharesByResource.ResourceID})
if err != nil {
g.logger.Debug().Err(err).Str("storage", sharesByResource.ResourceID.StorageId).Str("space", sharesByResource.ResourceID.SpaceId).Str("node", sharesByResource.ResourceID.OpaqueId).Msg("could not stat resource, skipping")
continue
}
item = *itemptr
}
var condition string
switch {
case item.Root != nil:
condition = unifiedrole.UnifiedRoleConditionDrive
case item.Folder != nil:
condition = unifiedrole.UnifiedRoleConditionFolder
case item.File != nil:
condition = unifiedrole.UnifiedRoleConditionFile
}
for _, share := range sharesByResource.Shares {
perm, err := g.cs3UserShareToPermission(ctx, share, condition)
var errcode errorcode.Error
switch {
case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
// The Grantee couldn't be found (user/group does not exist anymore)
continue
case err != nil:
return err
}
item.Permissions = append(item.Permissions, *perm)
}
if len(item.Permissions) == 0 {
continue
}
select {
case results <- &item:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
}
// Wait for things to settle down, then close results chan
go func() {
_ = errg.Wait() // error is checked later
close(results)
}()
for item := range results {
driveItems[item.GetId()] = *item
}
if err := errg.Wait(); err != nil {
return nil, err
}
return driveItems, nil
}
func (g BaseGraphService) cs3OCMSharesToDriveItems(ctx context.Context, shares []*ocm.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
errg, ctx := errgroup.WithContext(ctx)
// group shares by resource id
sharesByResource := make(map[string][]*ocm.Share)
for _, share := range shares {
sharesByResource[share.GetResourceId().String()] = append(sharesByResource[share.GetResourceId().String()], share)
}
type resourceShares struct {
ResourceID *storageprovider.ResourceId
Shares []*ocm.Share
}
work := make(chan resourceShares, len(shares))
results := make(chan *libregraph.DriveItem, len(shares))
// Distribute work
errg.Go(func() error {
defer close(work)
for _, shares := range sharesByResource {
select {
case work <- resourceShares{ResourceID: shares[0].GetResourceId(), Shares: shares}:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
// Spawn workers that'll concurrently work the queue
numWorkers := g.config.MaxConcurrency
if len(sharesByResource) < numWorkers {
numWorkers = len(sharesByResource)
}
for i := 0; i < numWorkers; i++ {
errg.Go(func() error {
for sharesByResource := range work {
resIDStr := storagespace.FormatResourceID(sharesByResource.ResourceID)
item, ok := driveItems[resIDStr]
if !ok {
itemptr, err := g.getDriveItem(ctx, &storageprovider.Reference{ResourceId: sharesByResource.ResourceID})
if err != nil {
g.logger.Debug().Err(err).Interface("Share", sharesByResource.ResourceID).Msg("could not stat ocm share, skipping")
continue
}
item = *itemptr
}
var condition string
switch {
case item.Folder != nil:
condition = unifiedrole.UnifiedRoleConditionFolderFederatedUser
case item.File != nil:
condition = unifiedrole.UnifiedRoleConditionFileFederatedUser
}
for _, share := range sharesByResource.Shares {
perm, err := g.cs3OCMShareToPermission(ctx, share, condition)
var errcode errorcode.Error
switch {
case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
// The Grantee couldn't be found (user/group does not exist anymore)
continue
case err != nil:
return err
}
item.Permissions = append(item.Permissions, *perm)
}
if len(item.Permissions) == 0 {
continue
}
select {
case results <- &item:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
}
// Wait for things to settle down, then close results chan
go func() {
_ = errg.Wait() // error is checked later
close(results)
}()
for item := range results {
driveItems[item.GetId()] = *item
}
if err := errg.Wait(); err != nil {
return nil, err
}
return driveItems, nil
}
func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share, roleCondition string) (*libregraph.Permission, error) {
perm := libregraph.Permission{}
perm.SetRoles([]string{})
if roleCondition != unifiedrole.UnifiedRoleConditionDrive {
perm.SetId(share.GetId().GetOpaqueId())
}
grantedTo := libregraph.SharePointIdentitySet{}
switch share.GetGrantee().GetType() {
case storageprovider.GranteeType_GRANTEE_TYPE_USER:
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")
// User does not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetUser(user)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("u:" + user.GetId())
}
}
case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
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")
// Group not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetGroup(group)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("g:" + group.GetId())
}
}
}
// set expiration date
if share.GetExpiration() != nil {
perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration()))
}
// set cTime
if share.GetCtime() != nil {
perm.SetCreatedDateTime(cs3TimestampToTime(share.GetCtime()))
}
role := unifiedrole.CS3ResourcePermissionsToRole(
g.availableRoles,
share.GetPermissions().GetPermissions(),
roleCondition,
false,
)
if role != nil {
perm.SetRoles([]string{role.GetId()})
} else {
actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(share.GetPermissions().GetPermissions())
// neither a role nor actions are set, we need to return "none" as a hint in the actions
if len(actions) == 0 {
actions = []string{"none"}
}
perm.SetLibreGraphPermissionsActions(actions)
perm.SetRoles(nil)
}
perm.SetGrantedToV2(grantedTo)
if share.GetCreator() != nil {
cs3Identity, err := cs3UserIdToIdentity(ctx, g.identityCache, share.GetCreator())
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
perm.SetInvitation(
libregraph.SharingInvitation{
InvitedBy: &libregraph.IdentitySet{
User: &cs3Identity,
},
},
)
}
return &perm, nil
}
func (g BaseGraphService) cs3OCMShareToPermission(ctx context.Context, share *ocm.Share, roleCondition string) (*libregraph.Permission, error) {
perm := libregraph.Permission{}
perm.SetRoles([]string{})
if roleCondition != unifiedrole.UnifiedRoleConditionDrive {
perm.SetId(share.GetId().GetOpaqueId())
}
grantedTo := libregraph.SharePointIdentitySet{}
// hm or use share.GetShareType() to determine the type of share???
switch share.GetGrantee().GetType() {
case storageprovider.GranteeType_GRANTEE_TYPE_USER:
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")
// User does not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetUser(user)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("u:" + user.GetId())
}
}
case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
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")
// Group not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetGroup(group)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("g:" + group.GetId())
}
}
}
// set expiration date
if share.GetExpiration() != nil {
perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration()))
}
// set cTime
if share.GetCtime() != nil {
perm.SetCreatedDateTime(cs3TimestampToTime(share.GetCtime()))
}
var permissions *storageprovider.ResourcePermissions
for _, role := range share.GetAccessMethods() {
if role.GetWebdavOptions().GetPermissions() != nil {
permissions = role.GetWebdavOptions().GetPermissions()
}
}
role := unifiedrole.CS3ResourcePermissionsToRole(
g.availableRoles,
permissions,
roleCondition,
true,
)
if role != nil {
perm.SetRoles([]string{role.GetId()})
} else {
actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions)
perm.SetLibreGraphPermissionsActions(actions)
perm.SetRoles(nil)
}
perm.SetGrantedToV2(grantedTo)
if share.GetCreator() != nil {
cs3Identity, err := cs3UserIdToIdentity(ctx, g.identityCache, share.GetCreator())
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
perm.SetInvitation(
libregraph.SharingInvitation{
InvitedBy: &libregraph.IdentitySet{
User: &cs3Identity,
},
},
)
}
return &perm, nil
}
func (g BaseGraphService) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
errg, ctx := errgroup.WithContext(ctx)
// group shares by resource id
sharesByResource := make(map[string][]*link.PublicShare)
for _, share := range shares {
sharesByResource[share.GetResourceId().String()] = append(sharesByResource[share.GetResourceId().String()], share)
}
type resourceShares struct {
ResourceID *storageprovider.ResourceId
Shares []*link.PublicShare
}
work := make(chan resourceShares, len(shares))
results := make(chan *libregraph.DriveItem, len(shares))
// Distribute work
errg.Go(func() error {
defer close(work)
for _, shares := range sharesByResource {
select {
case work <- resourceShares{ResourceID: shares[0].GetResourceId(), Shares: shares}:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
// Spawn workers that'll concurrently work the queue
numWorkers := g.config.MaxConcurrency
if len(sharesByResource) < numWorkers {
numWorkers = len(sharesByResource)
}
for i := 0; i < numWorkers; i++ {
errg.Go(func() error {
for sharesByResource := range work {
resIDStr := storagespace.FormatResourceID(sharesByResource.ResourceID)
item, ok := driveItems[resIDStr]
if !ok {
itemptr, err := g.getDriveItem(ctx, &storageprovider.Reference{ResourceId: sharesByResource.ResourceID})
if err != nil {
g.logger.Debug().Err(err).Interface("Share", sharesByResource.ResourceID).Msg("could not stat share, skipping")
continue
}
item = *itemptr
}
for _, share := range sharesByResource.Shares {
perm, err := g.libreGraphPermissionFromCS3PublicShare(share)
if err != nil {
g.logger.Error().Err(err).Interface("Link", sharesByResource.ResourceID).Msg("could not convert link to libregraph")
return err
}
item.Permissions = append(item.Permissions, *perm)
}
if len(item.Permissions) == 0 {
continue
}
select {
case results <- &item:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
}
// Wait for things to settle down, then close results chan
go func() {
_ = errg.Wait() // error is checked later
close(results)
}()
for item := range results {
driveItems[item.GetId()] = *item
}
if err := errg.Wait(); err != nil {
return nil, err
}
return driveItems, nil
}
func (g BaseGraphService) getLinkPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
cs3Share, err := g.getCS3PublicShareByID(ctx, permissionID)
if err != nil {
return nil, err
}
return cs3Share.GetResourceId(), nil
}
func (g BaseGraphService) getCS3PublicShareByID(ctx context.Context, permissionID string) (*link.PublicShare, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, err
}
getPublicShareResp, err := gatewayClient.GetPublicShare(ctx,
&link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(getPublicShareResp.GetStatus(), err); err != nil {
return nil, err
}
return getPublicShareResp.GetShare(), nil
}
func (g BaseGraphService) removeOCMPermission(ctx context.Context, permissionID string) error {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return err
}
removePublicShareResp, err := gatewayClient.RemoveOCMShare(ctx,
&ocm.RemoveOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(removePublicShareResp.GetStatus(), err); err != nil {
return err
}
// We need to return an untyped nil here otherwise the error==nil check won't work
return nil
}
func (g BaseGraphService) removePublicShare(ctx context.Context, permissionID string) error {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return err
}
removePublicShareResp, err := gatewayClient.RemovePublicShare(ctx,
&link.RemovePublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(removePublicShareResp.GetStatus(), err); err != nil {
return err
}
// We need to return an untyped nil here otherwise the error==nil check won't work
return nil
}
func (g BaseGraphService) removeUserShare(ctx context.Context, permissionID string) error {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return err
}
removeShareResp, err := gatewayClient.RemoveShare(ctx,
&collaboration.RemoveShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: &collaboration.ShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(removeShareResp.GetStatus(), err); err != nil {
return err
}
// We need to return an untyped nil here otherwise the error==nil check won't work
return nil
}
func (g BaseGraphService) removeSpacePermission(ctx context.Context, permissionID string, resourceId *storageprovider.ResourceId) error {
grantee, err := spacePermissionIdToCS3Grantee(permissionID)
if err != nil {
return err
}
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return err
}
removeShareResp, err := gatewayClient.RemoveShare(ctx, &collaboration.RemoveShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Key{
Key: &collaboration.ShareKey{
ResourceId: resourceId,
Grantee: &grantee,
},
},
},
})
if err := errorcode.FromCS3Status(removeShareResp.GetStatus(), err); err != nil {
return err
}
// We need to return an untyped nil here otherwise the error==nil check won't work
return nil
}
func (g BaseGraphService) getOCMPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
cs3Share, err := g.getCS3OCMShareByID(ctx, permissionID)
if err != nil {
return nil, err
}
return cs3Share.GetResourceId(), nil
}
func (g BaseGraphService) getCS3OCMShareByID(ctx context.Context, permissionID string) (*ocm.Share, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, err
}
getShareResp, err := gatewayClient.GetOCMShare(ctx,
&ocm.GetOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(getShareResp.GetStatus(), err); err != nil {
return nil, err
}
return getShareResp.GetShare(), nil
}
func (g BaseGraphService) getUserPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
cs3Share, err := g.getCS3UserShareByID(ctx, permissionID)
if err != nil {
return nil, err
}
return cs3Share.GetResourceId(), nil
}
func (g BaseGraphService) getCS3UserShareByID(ctx context.Context, permissionID string) (*collaboration.Share, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, err
}
getShareResp, err := gatewayClient.GetShare(ctx,
&collaboration.GetShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: &collaboration.ShareId{
OpaqueId: permissionID,
},
},
},
},
)
if err := errorcode.FromCS3Status(getShareResp.GetStatus(), err); err != nil {
return nil, err
}
return getShareResp.GetShare(), nil
}
func (g BaseGraphService) getOCMPermissionByID(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId) (*libregraph.Permission, *storageprovider.ResourceId, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelevtor failed")
return nil, nil, err
}
ocmShare, err := g.getCS3OCMShareByID(ctx, permissionID)
if err != nil {
return nil, nil, err
}
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
if err != nil {
return nil, nil, err
}
condition, err := roleConditionForResourceType(resourceInfo)
if err != nil {
return nil, nil, err
}
permission, err := g.cs3OCMShareToPermission(ctx, ocmShare, condition)
if err != nil {
return nil, nil, err
}
return permission, ocmShare.GetResourceId(), nil
}
func (g BaseGraphService) getPermissionByID(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId) (*libregraph.Permission, *storageprovider.ResourceId, error) {
var errcode errorcode.Error
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, nil, err
}
publicShare, err := g.getCS3PublicShareByID(ctx, permissionID)
switch {
case err == nil:
// the id is referencing a public share
permission, err := g.libreGraphPermissionFromCS3PublicShare(publicShare)
if err != nil {
return nil, nil, err
}
return permission, publicShare.GetResourceId(), nil
case IsSpaceRoot(itemID):
// itemID is referencing a spaceroot this is a space permission. Handle
// that here and get space id
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
if err != nil {
return nil, nil, err
}
perms, _, err := g.getSpaceRootPermissions(ctx, resourceInfo.GetSpace().GetId(), false)
if err != nil {
return nil, nil, err
}
for _, p := range perms {
if p.GetId() == permissionID {
return &p, itemID, nil
}
}
case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
// there is no public link with that id, check if this is a user share
cs3Share, err := g.getCS3UserShareByID(ctx, permissionID)
if err != nil {
return nil, nil, err
}
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
if err != nil {
return nil, nil, err
}
condition, err := roleConditionForResourceType(resourceInfo)
if err != nil {
return nil, nil, err
}
permission, err := g.cs3UserShareToPermission(ctx, cs3Share, condition)
if err != nil {
return nil, nil, err
}
return permission, cs3Share.GetResourceId(), nil
}
return nil, nil, err
}
func (g BaseGraphService) updateOCMPermission(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId, newPermission *libregraph.Permission) (*libregraph.Permission, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, err
}
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
if err != nil {
return nil, err
}
condition, err := federatedRoleConditionForResourceType(resourceInfo)
if err != nil {
return nil, err
}
var cs3UpdateOCMShareReq ocm.UpdateOCMShareRequest
cs3UpdateOCMShareReq.Ref = &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: permissionID,
},
},
}
if expiration, ok := newPermission.GetExpirationDateTimeOk(); ok {
cs3UpdateOCMShareReq.Field = append(
cs3UpdateOCMShareReq.Field,
&ocm.UpdateOCMShareRequest_UpdateField{
Field: &ocm.UpdateOCMShareRequest_UpdateField_Expiration{
Expiration: utils.TimeToTS(*expiration),
},
},
)
}
var allowedResourceActions []string
var permissionsUpdated bool
if roles, ok := newPermission.GetRolesOk(); ok {
if len(roles) > 0 {
for _, roleID := range roles {
role, err := unifiedrole.GetRole(unifiedrole.RoleFilterIDs(roleID))
if err != nil {
g.logger.Debug().Err(err).Interface("role", role).Msg("unable to convert requested role")
return nil, err
}
allowedResourceActions = unifiedrole.GetAllowedResourceActions(role, condition)
if len(allowedResourceActions) == 0 {
return nil, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource")
}
}
permissionsUpdated = true
} else if allowedResourceActions, ok = newPermission.GetLibreGraphPermissionsActionsOk(); ok && len(allowedResourceActions) > 0 {
permissionsUpdated = true
}
if permissionsUpdated {
cs3UpdateOCMShareReq.Field = append(cs3UpdateOCMShareReq.Field, &ocm.UpdateOCMShareRequest_UpdateField{
Field: &ocm.UpdateOCMShareRequest_UpdateField_AccessMethods{
AccessMethods: &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: &ocm.WebDAVAccessMethod{
Permissions: unifiedrole.PermissionsToCS3ResourcePermissions(
[]*libregraph.UnifiedRolePermission{
{
AllowedResourceActions: allowedResourceActions,
},
},
),
},
},
},
},
})
}
}
updateOCMShareResp, err := gatewayClient.UpdateOCMShare(ctx, &cs3UpdateOCMShareReq)
if err := errorcode.FromCS3Status(updateOCMShareResp.GetStatus(), err); err != nil {
return nil, err
}
ocmShareResp, err := gatewayClient.GetOCMShare(ctx, &ocm.GetOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: permissionID,
},
},
},
})
if err := errorcode.FromCS3Status(ocmShareResp.GetStatus(), err); err != nil {
return nil, err
}
permission, err := g.cs3OCMShareToPermission(ctx, ocmShareResp.GetShare(), condition)
if err != nil {
return nil, err
}
return permission, nil
}
func (g BaseGraphService) updateUserShare(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId, newPermission *libregraph.Permission) (*libregraph.Permission, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, err
}
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
if err != nil {
return nil, err
}
condition, err := roleConditionForResourceType(resourceInfo)
if err != nil {
return nil, err
}
var cs3UpdateShareReq collaboration.UpdateShareRequest
// When updating a space root we need to reference the share by resourceId and grantee
if IsSpaceRoot(itemID) {
grantee, err := spacePermissionIdToCS3Grantee(permissionID)
if err != nil {
g.logger.Debug().Err(err).Str("permissionid", permissionID).Msg("failed to parse space permission id")
return nil, err
}
cs3UpdateShareReq.Share = &collaboration.Share{
ResourceId: itemID,
Grantee: &grantee,
}
cs3UpdateShareReq.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"spacegrant": {},
},
}
cs3UpdateShareReq.Opaque = utils.AppendPlainToOpaque(cs3UpdateShareReq.Opaque, "spacetype", _spaceTypeProject)
} else {
cs3UpdateShareReq.Share = &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: permissionID,
},
}
}
fieldmask := []string{}
if expiration, ok := newPermission.GetExpirationDateTimeOk(); ok {
fieldmask = append(fieldmask, "expiration")
if expiration != nil {
cs3UpdateShareReq.Share.Expiration = utils.TimeToTS(*expiration)
}
}
var roles, allowedResourceActions []string
var permissionsUpdated, ok bool
if roles, ok = newPermission.GetRolesOk(); ok && len(roles) > 0 {
for _, roleID := range roles {
role, err := unifiedrole.GetRole(unifiedrole.RoleFilterIDs(roleID))
if err != nil {
g.logger.Debug().Err(err).Interface("role", role).Msg("unable to convert requested role")
return nil, err
}
allowedResourceActions = unifiedrole.GetAllowedResourceActions(role, condition)
if len(allowedResourceActions) == 0 && role.GetId() != unifiedrole.UnifiedRoleDeniedID {
return nil, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource")
}
}
permissionsUpdated = true
} else if allowedResourceActions, ok = newPermission.GetLibreGraphPermissionsActionsOk(); ok && len(allowedResourceActions) > 0 {
permissionsUpdated = true
}
if permissionsUpdated {
cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(
[]*libregraph.UnifiedRolePermission{
{
AllowedResourceActions: allowedResourceActions,
},
},
)
cs3UpdateShareReq.Share.Permissions = &collaboration.SharePermissions{
Permissions: cs3ResourcePermissions,
}
fieldmask = append(fieldmask, "permissions")
}
cs3UpdateShareReq.UpdateMask = &fieldmaskpb.FieldMask{
Paths: fieldmask,
}
updateUserShareResp, err := gatewayClient.UpdateShare(ctx, &cs3UpdateShareReq)
if err := errorcode.FromCS3Status(updateUserShareResp.GetStatus(), err); err != nil {
return nil, err
}
permission, err := g.cs3UserShareToPermission(ctx, updateUserShareResp.GetShare(), condition)
if err != nil {
return nil, err
}
return permission, nil
}