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/qsfera/server/pkg/log" "github.com/qsfera/server/services/graph/pkg/config" "github.com/qsfera/server/services/graph/pkg/errorcode" "github.com/qsfera/server/services/graph/pkg/identity" "github.com/qsfera/server/services/graph/pkg/identity/cache" "github.com/qsfera/server/services/graph/pkg/linktype" "github.com/qsfera/server/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 publicBaseURL *url.URL } 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, g.publicBaseURL, 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) { return g.listSharesWithSpaceRootFilter(ctx, false, filters, driveItems) } // listSpaceRootUserShares lists user/group shares whose resource is a space root (i.e. space // memberships). It uses SpaceRootFilter(true) so only space-root shares are returned, mirroring // how listUserShares works for regular file/folder shares. func (g BaseGraphService) listSpaceRootUserShares(ctx context.Context, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { return g.listSharesWithSpaceRootFilter(ctx, true, filters, driveItems) } func (g BaseGraphService) listSharesWithSpaceRootFilter(ctx context.Context, spaceRoot bool, 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(), share.SpaceRootFilter(spaceRoot), } 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{}) 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) } 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) } } // 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) 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 space root — use the share manager to look up the permission driveItems := make(driveItemsByResourceID) driveItems, err = g.listSpaceRootUserShares(ctx, []*collaboration.Filter{ share.ResourceIDFilter(itemID), }, driveItems) if err != nil { return nil, nil, err } for _, item := range driveItems { for i := range item.Permissions { if item.Permissions[i].GetId() == permissionID { return &item.Permissions[i], 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 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 }