Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ require (
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.3.2
github.com/go-chi/render v1.0.3
github.com/go-jose/go-jose/v3 v3.0.5
github.com/go-ldap/ldap/v3 v3.4.14
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/go-micro/plugins/v4/client/grpc v1.2.1
Expand Down Expand Up @@ -63,13 +62,14 @@ require (
github.com/onsi/gomega v1.42.1
github.com/open-policy-agent/opa v1.19.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902093522-448c55fc0d69
github.com/opencloud-eu/reva/v2 v2.49.0
github.com/opensearch-project/opensearch-go/v4 v4.7.3
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
github.com/prometheus/client_golang v1.24.1
github.com/prometheus/client_model v0.6.2
github.com/r3labs/sse/v2 v2.10.0
github.com/riandyrn/otelchi v0.12.3
github.com/rogpeppe/go-internal v1.16.0
Expand Down Expand Up @@ -207,6 +207,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
Expand Down Expand Up @@ -324,7 +325,6 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.33.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -940,8 +940,8 @@ github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-202505121527
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55 h1:dzYZ5iA6i0QoFUap9l6sZ4Ts2HXiXVfsOoiRYToYgm8=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902093522-448c55fc0d69 h1:LLcyQwhfGqBr4wxLgAwnT9ohFtsBaEdhPPG5KII02tI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902093522-448c55fc0d69/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/reva/v2 v2.49.0 h1:AwECMDDth3NUaihZRf9bI9HNpWutvaRijOCvQyQfTT4=
github.com/opencloud-eu/reva/v2 v2.49.0/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
Expand Down
15 changes: 8 additions & 7 deletions services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ type Config struct {
TokenManager *TokenManager `yaml:"token_manager"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`

Application Application `yaml:"application"`
Spaces Spaces `yaml:"spaces"`
Identity Identity `yaml:"identity"`
IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"1.0.0"`
Events Events `yaml:"events"`
UnifiedRoles UnifiedRoles `yaml:"unified_roles"`
MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;GRAPH_MAX_CONCURRENCY" desc:"The maximum number of concurrent requests the service will handle." introductionVersion:"1.0.0"`
Application Application `yaml:"application"`
Spaces Spaces `yaml:"spaces"`
Identity Identity `yaml:"identity"`
IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"1.0.0"`
EnableGuestInvites bool `yaml:"enable_guest_invites" env:"GRAPH_ENABLE_GUEST_INVITES" desc:"Enables creating permission invites (shares) to mail addresses. Disabled by default." introductionVersion:"%NEXT%"`
Events Events `yaml:"events"`
UnifiedRoles UnifiedRoles `yaml:"unified_roles"`
MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;GRAPH_MAX_CONCURRENCY" desc:"The maximum number of concurrent requests the service will handle." introductionVersion:"1.0.0"`

Keycloak Keycloak `yaml:"keycloak"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Expand Down
3 changes: 2 additions & 1 deletion services/graph/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ func DefaultConfig() *config.Config {
AssignDefaultUserRole: true,
IdentitySearchMinLength: 3,
},
Reva: shared.DefaultRevaConfig(),
EnableGuestInvites: false,
Reva: shared.DefaultRevaConfig(),
Spaces: config.Spaces{
StorageUsersAddress: "eu.opencloud.api.storage-users",
WebDavBase: "https://localhost:9200",
Expand Down
180 changes: 130 additions & 50 deletions services/graph/pkg/service/v0/api_driveitem_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"net/mail"
"net/url"
"slices"
"strings"
Expand All @@ -14,6 +15,8 @@ import (
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/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"
Expand Down Expand Up @@ -163,85 +166,142 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
var shareid string
var expiration *types.Timestamp
var cTime *types.Timestamp
switch driveRecipient.GetLibreGraphRecipientType() {
case "group":
group, err := s.identityCache.GetGroup(ctx, objectID)
if err != nil {
s.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
if email := driveRecipient.GetEmail(); email != "" {
if !s.config.EnableGuestInvites {
return libregraph.Permission{}, errorcode.New(errorcode.NotSupported, "sharing with mail recipients is not enabled")
}
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
Group: &libregraph.Identity{
DisplayName: group.GetDisplayName(),
Id: conversions.ToPointer(group.GetId()),
user := revactx.ContextMustGetUser(ctx)
rsp, err := gatewayClient.CheckPermission(ctx, &permissionsapi.CheckPermissionRequest{
Permission: "GuestInvites.Create",
SubjectRef: &permissionsapi.SubjectReference{
Spec: &permissionsapi.SubjectReference_UserId{UserId: user.GetId()},
},
})
if err != nil || rsp.GetStatus().GetCode() != rpc.Code_CODE_OK {
return libregraph.Permission{}, errorcode.New(errorcode.NotAllowed, "permission denied")
}
email = strings.TrimSpace(email)
if len(email) == 0 {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid mail recipient")
}

parsedMail, err := mail.ParseAddress(email)
if err != nil {
s.logger.Debug().Err(err).Msg("failed to parse mail recipient")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid mail recipient")
}
createShareRequest := createShareRequestToGroup(group, statResponse.GetInfo(), cs3ResourcePermissions)

// we're only interested in the Address part of the mail address (this is what reva uses as the user id
// for the created share and grants) let's strip on any "Name" part that might be existing
email = parsedMail.Address

createShareRequest := createShareRequestToMail(email, statResponse.GetInfo(), cs3ResourcePermissions)

if invite.ExpirationDateTime != nil {
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)

if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Debug().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
default:
user, err := s.identityCache.GetCS3User(ctx, tenantId, objectID)
if errors.Is(err, identity.ErrNotFound) && s.config.IncludeOCMSharees {
user, err = s.identityCache.GetAcceptedCS3User(ctx, objectID)
if err == nil && IsSpaceRoot(statResponse.GetInfo().GetId()) {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "federated user can not become a space member")
}
}
if err != nil {
s.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())

identity := &libregraph.Identity{
Id: conversions.ToPointer(email),
DisplayName: email,
LibreGraphUserType: conversions.ToPointer("Mail"),
}

permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
User: &libregraph.Identity{
DisplayName: user.GetDisplayName(),
Id: conversions.ToPointer(user.GetId().GetOpaqueId()),
LibreGraphUserType: conversions.ToPointer(identity.CS3UserTypeToGraph(user.GetId().GetType())),
},
User: identity,
}

if user.GetId().GetType() == userpb.UserType_USER_TYPE_FEDERATED {
providerInfoResp, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: user.GetId().GetIdp(),
})
if err = errorcode.FromCS3Status(providerInfoResp.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("getting provider info failed")
return libregraph.Permission{}, err
}

createShareRequest := createShareRequestToFederatedUser(user, statResponse.GetInfo().GetId(), providerInfoResp.ProviderInfo, cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
} else {
switch driveRecipient.GetLibreGraphRecipientType() {
case "group":
group, err := s.identityCache.GetGroup(ctx, objectID)
if err != nil {
s.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
createShareResponse, err := gatewayClient.CreateOCMShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
Group: &libregraph.Identity{
DisplayName: group.GetDisplayName(),
Id: conversions.ToPointer(group.GetId()),
},
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
} else {
createShareRequest := createShareRequestToUser(user, statResponse.GetInfo(), cs3ResourcePermissions)
createShareRequest := createShareRequestToGroup(group, statResponse.GetInfo(), cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Debug().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
default:
user, err := s.identityCache.GetCS3User(ctx, tenantId, objectID)
if errors.Is(err, identity.ErrNotFound) && s.config.IncludeOCMSharees {
user, err = s.identityCache.GetAcceptedCS3User(ctx, objectID)
if err == nil && IsSpaceRoot(statResponse.GetInfo().GetId()) {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "federated user can not become a space member")
}
}
if err != nil {
s.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
User: &libregraph.Identity{
DisplayName: user.GetDisplayName(),
Id: conversions.ToPointer(user.GetId().GetOpaqueId()),
LibreGraphUserType: conversions.ToPointer(identity.CS3UserTypeToGraph(user.GetId().GetType())),
},
}

if user.GetId().GetType() == userpb.UserType_USER_TYPE_FEDERATED {
providerInfoResp, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: user.GetId().GetIdp(),
})
if err = errorcode.FromCS3Status(providerInfoResp.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("getting provider info failed")
return libregraph.Permission{}, err
}

createShareRequest := createShareRequestToFederatedUser(user, statResponse.GetInfo().GetId(), providerInfoResp.ProviderInfo, cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateOCMShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
} else {
createShareRequest := createShareRequestToUser(user, statResponse.GetInfo(), cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
}

}

}
Expand Down Expand Up @@ -331,6 +391,26 @@ func createShareRequestToFederatedUser(user *userpb.User, resourceId *storagepro
}
}

func createShareRequestToMail(mail string, info *storageprovider.ResourceInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *collaboration.CreateShareRequest {
return &collaboration.CreateShareRequest{
ResourceInfo: info,
Grant: &collaboration.ShareGrant{
Grantee: &storageprovider.Grantee{
Type: storageprovider.GranteeType_GRANTEE_TYPE_USER,
Id: &storageprovider.Grantee_UserId{
UserId: &userpb.UserId{
Type: userpb.UserType_USER_TYPE_GUEST,
OpaqueId: mail,
},
},
},
Permissions: &collaboration.SharePermissions{
Permissions: cs3ResourcePermissions,
},
},
}
}

// SpaceRootInvite handles invitation request on project spaces
func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveID *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
Expand Down
Loading