Files
Pascal Bleser 5b72318493 chore(graph): add metrics for HTTP API and LDAP
Introducing gowrap as a build-time tool to generate interface delegate
structs from templates:

 * added as a 'make go-generate' target in services/graph,
 * added as a build-time dependency in .bingo/

Introduce an LDAP client abstraction interface to be able to wrap the
go-ldap client API with metrics transparently (and possibly hooks and
such in the future), in order to use delegation patterns to measure the
time LDAP (client) operations take to finish, as well as to track their
results (success, failure, not-found).

Has two implementations that are generated using gowrap:

* a go-ldap adapter implementation that directly delegates to a go-ldap
  connection
* a time measuring and metrics collecting implementation that delegates
  to another LdapClient

The metrics collecting one is disabled by default, can be enabled with
GRAPH_LDAP_METRICS_DISABLE=false

It collects durations of outbound LDAP client operations into a histogram, as
well as the number of concurrent outbound LDAP operations in a gauge (via an
atomic int and a gauge function, as that performs best).

Add an HTTP middleware that measures how long Graph HTTP API requests
take, storing taken time into a histogram along with labels for

 * method,
 * path pattern (from the chi routes),
 * Graph API version prefix,
 * Graph API resource name,
 * and the resulting status code.

It also tracks the number of concurrent inbound Graph API HTTP requests
using a gauge (also using an atomic int and a gauge function).

Disabled by default, can be enabled with
GRAPH_HTTP_METRICS_DISABLE=false

Add Backend and EducationBackend delegate implementations that measure
execution time on the level of the higher API call operations there
(CreateUser, DeleteUser, ..., CreateSchool, ...), generated using
gowrap.

Disabled by default, can be enabled with
GRAPH_IDENTITY_BACKEND_METRICS_DISABLE=false

Also added a small k6 script to produce some read-only load on the Graph
API, for a casual test of the metrics, as well as k6 in mise.toml.

Make an internal changes to how singular LDAP entry searches work in the LDAP
identity backends:

 * check whether searches for a singular entry returns more than one
   result, in which case a new error TooManyResults is returned, instead
   of leaving that undetected, blindly taking the first result, and
   potentially risking data inconsistencies

Improve the loggers in identity backends by adding attributes for their
request targets (Reva gateway address or LDAP URI, respectively).

Also add a "backend" attribute for all Graph API logs (set to "ldap" or
"cs3"), to help debug potential issues, and remove them from all the logger
debug calls at the beginning of each LDAP-related function as those should
really be part of the logger and set beforehand.

The LDAP identity backend logger also has two new attributes to help
debugging with logs:

 * write (bool): whether write operations are enabled
 * refint (bool): whether refint is enabled or not

Also adds a dedicated counter metric for user password change operations.

Minor campfire improvements:

 * add a constructor func for the CS3 backend

 * add a constructor func for the LDAP backend

 * in the LDAP identity backend, in searchLDAPEntryByFilter (used by all
   search/get public functions), errors that occur when performing LDAP
   SEARCH operations were blindly mapped to a ItemNotFound error,
   instead of being analyzed as it could be caused by a technical error

 * in the requireadmin middleware, add debug logging to explain why a
   request is denied

 * when an LDAP password change fails because the user entry was not
   found in LDAP, we now have a log message that tracks that
2026-09-01 10:57:47 +02:00

1262 lines
40 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
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)
if err != nil {
if errorcode.IsErrorCode(err, errorcode.ItemNotFound) {
// The Grantee couldn't be found (user/group does not exist anymore)
continue
} else {
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)
if err != nil {
if errorcode.IsErrorCode(err, errorcode.ItemNotFound) {
// The Grantee couldn't be found (user/group does not exist anymore)
continue
} else {
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) {
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 errorcode.IsErrorCode(err, 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
}