From 8feba5d63ec8d1bc9c0505255cadda1bdc1b09d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 29 Apr 2026 13:24:24 +0200 Subject: [PATCH 1/6] feat(graph): Add support creating guest(mail) permissions --- .../service/v0/api_driveitem_permissions.go | 61 +++++++++++++++++++ .../v0/api_driveitem_permissions_test.go | 38 ++++++++++++ services/graph/pkg/service/v0/utils.go | 22 +++++-- services/graph/pkg/validate/libregraph.go | 2 +- 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 63d9ec49af..55bedae04c 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/mail" "net/url" "slices" "strings" @@ -188,6 +189,46 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto shareid = createShareResponse.GetShare().GetId().GetOpaqueId() cTime = createShareResponse.GetShare().GetCtime() expiration = createShareResponse.GetShare().GetExpiration() + case "mail": + email := strings.TrimSpace(objectID) + 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") + } + + // 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() + + identity := &libregraph.Identity{ + Id: conversions.ToPointer(email), + DisplayName: email, + LibreGraphUserType: conversions.ToPointer("Mail"), + } + + permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ + User: identity, + } + default: user, err := s.identityCache.GetCS3User(ctx, tenantId, objectID) if errors.Is(err, identity.ErrNotFound) && s.config.IncludeOCMSharees { @@ -331,6 +372,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() diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index 4a088664dd..ef69e938ae 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -167,6 +167,43 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(permission.GrantedToV2.Group.GetId()).To(Equal("2")) }) + It("creates guest share using an email address", func() { + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("Test User "), LibreGraphRecipientType: libregraph.PtrString("mail")}, + } + createShareResponse.Share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "guest123"}, + } + + permission, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + Expect(permission.GetId()).To(Equal("guest123")) + Expect(permission.GrantedToV2.User.GetDisplayName()).To(Equal("guest@example.com")) + Expect(permission.GrantedToV2.User.GetId()).To(Equal("guest@example.com")) + Expect(permission.GrantedToV2.User.GetLibreGraphUserType()).To(Equal("Mail")) + }) + It("verifies that invalid email addresses are handled", func() { + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("invalid"), LibreGraphRecipientType: libregraph.PtrString("mail")}, + } + + _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid mail recipient")) + }) + + It("verifies that empty email addresses are handled", func() { + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString(" "), LibreGraphRecipientType: libregraph.PtrString("mail")}, + } + + _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid mail recipient")) + }) + It("succeeds with file roles (happy path)", func() { gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) @@ -326,6 +363,7 @@ var _ = Describe("DriveItemPermissionsService", func() { gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil) gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil) + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index 5927e812e1..ecdf0a0007 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -121,16 +121,30 @@ func federatedIdToIdentity(ctx context.Context, cache cache.IdentityCache, cs3Us return identity, err } +// guestMailToIdentity converts a USER_TYPE_GUEST (used for guest invites vial mail) into a libregraph.Identity +func guestMailToIdentity(cs3UserID *cs3User.UserId) (libregraph.Identity, error) { + identity := libregraph.Identity{ + Id: libregraph.PtrString(cs3UserID.GetOpaqueId()), + LibreGraphUserType: libregraph.PtrString("Guest"), + } + identity.SetDisplayName(cs3UserID.GetOpaqueId()) + identity.SetLibreGraphUserType("Guest") + return identity, nil +} + // cs3UserIdToIdentity looks up the user for the supplied cs3 userid using the cache and returns it // as a libregraph.Identity. Skips the user lookup if the id type is USER_TYPE_SPACE_OWNER func cs3UserIdToIdentity(ctx context.Context, cache cache.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { - if cs3UserID.GetType() == cs3User.UserType_USER_TYPE_FEDERATED { + switch cs3UserID.GetType() { + case cs3User.UserType_USER_TYPE_FEDERATED: return federatedIdToIdentity(ctx, cache, cs3UserID) - } - if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER { + case cs3User.UserType_USER_TYPE_GUEST: + return guestMailToIdentity(cs3UserID) + case cs3User.UserType_USER_TYPE_SPACE_OWNER: + return libregraph.Identity{Id: libregraph.PtrString(cs3UserID.GetOpaqueId())}, nil + default: return userIdToIdentity(ctx, cache, cs3UserID.GetTenantId(), cs3UserID.GetOpaqueId()) } - return libregraph.Identity{Id: libregraph.PtrString(cs3UserID.GetOpaqueId())}, nil } // groupIdToIdentity looks up the group for the supplied cs3 groupid using the cache and returns it diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go index 572427b6b1..77fc98586f 100644 --- a/services/graph/pkg/validate/libregraph.go +++ b/services/graph/pkg/validate/libregraph.go @@ -47,7 +47,7 @@ func libregraphDriveItemInvite(v *validator.Validate) { func libregraphDriveRecipient(v *validator.Validate) { v.RegisterStructValidationMapRules(map[string]string{ "ObjectId": "ne=", - "LibreGraphRecipientType": "oneof=user group", + "LibreGraphRecipientType": "oneof=user group mail", }, libregraph.DriveRecipient{}) } From c23fb81647432b824692f7963867037cf43e9e37 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 1 Sep 2026 13:43:21 +0200 Subject: [PATCH 2/6] chore(graph): use existing constants for user type --- .../graph/pkg/service/v0/educationuser.go | 3 ++- services/graph/pkg/service/v0/users.go | 2 +- services/graph/pkg/service/v0/users_filter.go | 5 +++-- services/graph/pkg/service/v0/utils.go | 21 ++++++++++--------- services/graph/pkg/unifiedrole/roles.go | 3 ++- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/services/graph/pkg/service/v0/educationuser.go b/services/graph/pkg/service/v0/educationuser.go index 389eccf665..bbbf09b0dc 100644 --- a/services/graph/pkg/service/v0/educationuser.go +++ b/services/graph/pkg/service/v0/educationuser.go @@ -16,6 +16,7 @@ import ( libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" + "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -115,7 +116,7 @@ func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) { return } } else { - u.SetUserType("Member") + u.SetUserType(identity.UserTypeMember) } logger.Debug().Interface("user", u).Msg("calling create education user on backend") diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 893464890c..0a668e4929 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -405,7 +405,7 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "userType is a read-only attribute") return } - u.SetUserType("Member") + u.SetUserType(identity.UserTypeMember) logger.Debug().Interface("user", u).Msg("calling create user on backend") if u, err = g.identityBackend.CreateUser(r.Context(), *u); err != nil { diff --git a/services/graph/pkg/service/v0/users_filter.go b/services/graph/pkg/service/v0/users_filter.go index 0232e06f2e..95ecaa151f 100644 --- a/services/graph/pkg/service/v0/users_filter.go +++ b/services/graph/pkg/service/v0/users_filter.go @@ -8,6 +8,7 @@ import ( libregraph "github.com/opencloud-eu/libre-graph-api-go" settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0" settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" + "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" ) const ( @@ -240,9 +241,9 @@ func (g Graph) applyFilterEq(ctx context.Context, req *godata.GoDataRequest, ope // unquote value := strings.Trim(operand2.Token.Value, "'") switch value { - case "Member", "Guest": + case identity.UserTypeMember, identity.UserTypeGuest: return g.identityBackend.GetUsers(ctx, req) - case "Federated": + case identity.UserTypeFederated: return g.searchOCMAcceptedUsers(ctx, req) } return users, unsupportedFilterError() diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index ecdf0a0007..29b77e8b32 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -21,6 +21,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" "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/unifiedrole" ) @@ -109,27 +110,27 @@ func userIdToIdentity(ctx context.Context, cache cache.IdentityCache, tennantId, // as a libregraph.Identity func federatedIdToIdentity(ctx context.Context, cache cache.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) { userID := fmt.Sprintf("%s@%s", cs3UserID.GetOpaqueId(), cs3UserID.GetIdp()) - identity := libregraph.Identity{ + lgIdentity := libregraph.Identity{ Id: libregraph.PtrString(userID), - LibreGraphUserType: libregraph.PtrString("Federated"), + LibreGraphUserType: libregraph.PtrString(identity.UserTypeFederated), } user, err := cache.GetAcceptedUser(ctx, userID) if err == nil { - identity.SetDisplayName(user.GetDisplayName()) - identity.SetLibreGraphUserType(user.GetUserType()) + lgIdentity.SetDisplayName(user.GetDisplayName()) + lgIdentity.SetLibreGraphUserType(user.GetUserType()) } - return identity, err + return lgIdentity, err } // guestMailToIdentity converts a USER_TYPE_GUEST (used for guest invites vial mail) into a libregraph.Identity func guestMailToIdentity(cs3UserID *cs3User.UserId) (libregraph.Identity, error) { - identity := libregraph.Identity{ + lgIdentity := libregraph.Identity{ Id: libregraph.PtrString(cs3UserID.GetOpaqueId()), - LibreGraphUserType: libregraph.PtrString("Guest"), + LibreGraphUserType: libregraph.PtrString(identity.UserTypeGuest), } - identity.SetDisplayName(cs3UserID.GetOpaqueId()) - identity.SetLibreGraphUserType("Guest") - return identity, nil + lgIdentity.SetDisplayName(cs3UserID.GetOpaqueId()) + lgIdentity.SetLibreGraphUserType(identity.UserTypeGuest) + return lgIdentity, nil } // cs3UserIdToIdentity looks up the user for the supplied cs3 userid using the cache and returns it diff --git a/services/graph/pkg/unifiedrole/roles.go b/services/graph/pkg/unifiedrole/roles.go index 1420eeab94..52aeb0d2ab 100644 --- a/services/graph/pkg/unifiedrole/roles.go +++ b/services/graph/pkg/unifiedrole/roles.go @@ -11,6 +11,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/conversions" "github.com/opencloud-eu/opencloud/pkg/l10n" + "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" graphl10n "github.com/opencloud-eu/opencloud/services/graph/pkg/l10n" ) @@ -70,7 +71,7 @@ const ( // .UserType is the type of the user: 'Member' for a member of the organization, 'Guest' for a guest user, 'Federated' for a federated user. // UnifiedRoleConditionFederatedUser defines a constraint that matches a federated user - UnifiedRoleConditionFederatedUser = "@Subject.UserType==\"Federated\"" + UnifiedRoleConditionFederatedUser = "@Subject.UserType==\"" + identity.UserTypeFederated + "\"" // For federated sharing we need roles that combine the constraints for the resource and the user. // UnifiedRoleConditionFileFederatedUser defines a constraint that matches a File and a federated user From 8ec74a7bf3b65ab92c7e6777d07f9788f9087980 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 1 Sep 2026 17:55:39 +0200 Subject: [PATCH 3/6] chore(graph): add config knob to disable/enable guest invites --- services/graph/pkg/config/config.go | 15 ++++++++------- .../graph/pkg/config/defaults/defaultconfig.go | 3 ++- .../pkg/service/v0/api_driveitem_permissions.go | 3 +++ .../service/v0/api_driveitem_permissions_test.go | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 2a84a5a4e9..dba743c8d6 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -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"` diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 159b7db510..c2c669469d 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -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", diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 55bedae04c..2c9c186df2 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -190,6 +190,9 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto cTime = createShareResponse.GetShare().GetCtime() expiration = createShareResponse.GetShare().GetExpiration() case "mail": + if !s.config.EnableGuestInvites { + return libregraph.Permission{}, errorcode.New(errorcode.NotSupported, "sharing with mail recipients is not enabled") + } email := strings.TrimSpace(objectID) if len(email) == 0 { return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid mail recipient") diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index ef69e938ae..523fa0fa34 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -168,6 +168,7 @@ var _ = Describe("DriveItemPermissionsService", func() { }) It("creates guest share using an email address", func() { + cfg.EnableGuestInvites = true gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ @@ -185,6 +186,7 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(permission.GrantedToV2.User.GetLibreGraphUserType()).To(Equal("Mail")) }) It("verifies that invalid email addresses are handled", func() { + cfg.EnableGuestInvites = true driveItemInvite.Recipients = []libregraph.DriveRecipient{ {ObjectId: libregraph.PtrString("invalid"), LibreGraphRecipientType: libregraph.PtrString("mail")}, } @@ -195,6 +197,7 @@ var _ = Describe("DriveItemPermissionsService", func() { }) It("verifies that empty email addresses are handled", func() { + cfg.EnableGuestInvites = true driveItemInvite.Recipients = []libregraph.DriveRecipient{ {ObjectId: libregraph.PtrString(" "), LibreGraphRecipientType: libregraph.PtrString("mail")}, } @@ -204,6 +207,17 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(err.Error()).To(ContainSubstring("invalid mail recipient")) }) + It("rejects guest shares when guest invites are disabled by default", func() { + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("guest@example.com"), LibreGraphRecipientType: libregraph.PtrString("mail")}, + } + + _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not enabled")) + }) + It("succeeds with file roles (happy path)", func() { gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) From b02ba2b3f6375db0b96359d089351f3fce0021f6 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 2 Sep 2026 13:44:33 +0200 Subject: [PATCH 4/6] bumped libre graph api version, using recipient email check instead of recipient type in invite creation --- go.mod | 6 +- go.sum | 4 +- .../service/v0/api_driveitem_permissions.go | 144 +++++++++--------- .../v0/api_driveitem_permissions_test.go | 8 +- services/graph/pkg/validate/libregraph.go | 17 ++- .../graph/pkg/validate/libregraph_test.go | 20 +++ .../opencloud-eu/libre-graph-api-go/README.md | 2 + .../libre-graph-api-go/api_drive_item.go | 20 +++ .../libre-graph-api-go/api_drives_root.go | 10 ++ .../libre-graph-api-go/api_me_drive_root.go | 10 ++ .../libre-graph-api-go/model_drive_item.go | 73 +++++++++ .../model_drive_recipient.go | 37 +++++ .../model_pending_operations.go | 126 +++++++++++++++ ...nding_operations_pending_content_update.go | 128 ++++++++++++++++ vendor/modules.txt | 2 +- 15 files changed, 525 insertions(+), 82 deletions(-) create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations_pending_content_update.go diff --git a/go.mod b/go.mod index b609e9ef20..5b32a2c3a7 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 6ffc6d9bba..c75fec207b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 2c9c186df2..3949a8e35b 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -164,36 +164,11 @@ 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()) - } - permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ - Group: &libregraph.Identity{ - DisplayName: group.GetDisplayName(), - Id: conversions.ToPointer(group.GetId()), - }, - } - 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.Debug().Err(err).Msg("share creation failed") - return libregraph.Permission{}, err - } - shareid = createShareResponse.GetShare().GetId().GetOpaqueId() - cTime = createShareResponse.GetShare().GetCtime() - expiration = createShareResponse.GetShare().GetExpiration() - case "mail": + if email := driveRecipient.GetEmail(); email != "" { if !s.config.EnableGuestInvites { return libregraph.Permission{}, errorcode.New(errorcode.NotSupported, "sharing with mail recipients is not enabled") } - email := strings.TrimSpace(objectID) + email = strings.TrimSpace(email) if len(email) == 0 { return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid mail recipient") } @@ -214,6 +189,7 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto 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 @@ -232,60 +208,88 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto User: identity, } - 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) + } 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() + } + } } diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index 523fa0fa34..71887cd4c1 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -172,7 +172,7 @@ var _ = Describe("DriveItemPermissionsService", func() { gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("Test User "), LibreGraphRecipientType: libregraph.PtrString("mail")}, + {Email: libregraph.PtrString("Test User ")}, } createShareResponse.Share = &collaboration.Share{ Id: &collaboration.ShareId{OpaqueId: "guest123"}, @@ -188,7 +188,7 @@ var _ = Describe("DriveItemPermissionsService", func() { It("verifies that invalid email addresses are handled", func() { cfg.EnableGuestInvites = true driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("invalid"), LibreGraphRecipientType: libregraph.PtrString("mail")}, + {Email: libregraph.PtrString("invalid")}, } _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) @@ -199,7 +199,7 @@ var _ = Describe("DriveItemPermissionsService", func() { It("verifies that empty email addresses are handled", func() { cfg.EnableGuestInvites = true driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString(" "), LibreGraphRecipientType: libregraph.PtrString("mail")}, + {Email: libregraph.PtrString(" ")}, } _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) @@ -209,7 +209,7 @@ var _ = Describe("DriveItemPermissionsService", func() { It("rejects guest shares when guest invites are disabled by default", func() { driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("guest@example.com"), LibreGraphRecipientType: libregraph.PtrString("mail")}, + {Email: libregraph.PtrString("guest@example.com")}, } _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go index 77fc98586f..9932ede0a5 100644 --- a/services/graph/pkg/validate/libregraph.go +++ b/services/graph/pkg/validate/libregraph.go @@ -46,8 +46,21 @@ func libregraphDriveItemInvite(v *validator.Validate) { // libregraphDriveRecipient validates libregraph.DriveRecipient func libregraphDriveRecipient(v *validator.Validate) { v.RegisterStructValidationMapRules(map[string]string{ - "ObjectId": "ne=", - "LibreGraphRecipientType": "oneof=user group mail", + "ObjectId": "omitempty,ne=", + "Email": "omitempty,email", + "LibreGraphRecipientType": "oneof=user group", + }, libregraph.DriveRecipient{}) + + v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) { + driveRecipient := sl.Current().Interface().(libregraph.DriveRecipient) + + if driveRecipient.GetObjectId() == "" && driveRecipient.GetEmail() == "" { + sl.ReportError(driveRecipient.ObjectId, "ObjectId", "objectId", "oneof", "either objectId or email is required") + return + } + if driveRecipient.GetObjectId() != "" && driveRecipient.GetEmail() != "" { + sl.ReportError(driveRecipient.ObjectId, "ObjectId", "objectId", "exclusive", "objectId and email are mutually exclusive") + } }, libregraph.DriveRecipient{}) } diff --git a/services/graph/pkg/validate/libregraph_test.go b/services/graph/pkg/validate/libregraph_test.go index d7f189764a..32992022ca 100644 --- a/services/graph/pkg/validate/libregraph_test.go +++ b/services/graph/pkg/validate/libregraph_test.go @@ -147,6 +147,26 @@ var _ = Describe("libregraph", func() { return driveRecipient, false }, ), + Entry("succeed: email recipient", + func() (libregraph.DriveRecipient, bool) { + driveRecipient.ObjectId = nil + driveRecipient.Email = conversions.ToPointer("guest@example.com") + return driveRecipient, true + }, + ), + Entry("fail: invalid email", + func() (libregraph.DriveRecipient, bool) { + driveRecipient.ObjectId = nil + driveRecipient.Email = conversions.ToPointer("invalid") + return driveRecipient, false + }, + ), + Entry("fail: objectId and email both set", + func() (libregraph.DriveRecipient, bool) { + driveRecipient.Email = conversions.ToPointer("guest@example.com") + return driveRecipient, false + }, + ), Entry("succeed: valid role", func() (libregraph.DriveRecipient, bool) { driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user") diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md index eadd712142..816ad6e916 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md @@ -242,6 +242,8 @@ Class | Method | HTTP request | Description - [OpenGraphFile](docs/OpenGraphFile.md) - [PasswordChange](docs/PasswordChange.md) - [PasswordProfile](docs/PasswordProfile.md) + - [PendingOperations](docs/PendingOperations.md) + - [PendingOperationsPendingContentUpdate](docs/PendingOperationsPendingContentUpdate.md) - [Permission](docs/Permission.md) - [Photo](docs/Photo.md) - [Quota](docs/Quota.md) diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go index 598d9203c5..286f9658e2 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go @@ -350,6 +350,7 @@ type ApiGetDriveItemRequest struct { driveId string itemId string select_ *[]string + expand *[]string } // Select additional properties to be returned. @@ -358,6 +359,12 @@ func (r ApiGetDriveItemRequest) Select_(select_ []string) ApiGetDriveItemRequest return r } +// Expand related entities to be returned. +func (r ApiGetDriveItemRequest) Expand(expand []string) ApiGetDriveItemRequest { + r.expand = &expand + return r +} + func (r ApiGetDriveItemRequest) Execute() (*DriveItem, *http.Response, error) { return r.ApiService.GetDriveItemExecute(r) } @@ -408,6 +415,9 @@ func (a *DriveItemApiService) GetDriveItemExecute(r ApiGetDriveItemRequest) (*Dr if r.select_ != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "$select", r.select_, "form", "csv") } + if r.expand != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "$expand", r.expand, "form", "csv") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -755,6 +765,7 @@ type ApiGetDriveItemV1Request struct { driveId string itemId string select_ *[]string + expand *[]string } // Select additional properties to be returned. @@ -763,6 +774,12 @@ func (r ApiGetDriveItemV1Request) Select_(select_ []string) ApiGetDriveItemV1Req return r } +// Expand related entities to be returned. +func (r ApiGetDriveItemV1Request) Expand(expand []string) ApiGetDriveItemV1Request { + r.expand = &expand + return r +} + func (r ApiGetDriveItemV1Request) Execute() (*DriveItem, *http.Response, error) { return r.ApiService.GetDriveItemV1Execute(r) } @@ -816,6 +833,9 @@ func (a *DriveItemApiService) GetDriveItemV1Execute(r ApiGetDriveItemV1Request) if r.select_ != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "$select", r.select_, "form", "csv") } + if r.expand != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "$expand", r.expand, "form", "csv") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drives_root.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drives_root.go index ee0b03a324..977c75e546 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drives_root.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drives_root.go @@ -548,6 +548,7 @@ type ApiGetRootRequest struct { ApiService *DrivesRootApiService driveId string select_ *[]string + expand *[]string } // Select additional properties to be returned. @@ -556,6 +557,12 @@ func (r ApiGetRootRequest) Select_(select_ []string) ApiGetRootRequest { return r } +// Expand related entities to be returned. +func (r ApiGetRootRequest) Expand(expand []string) ApiGetRootRequest { + r.expand = &expand + return r +} + func (r ApiGetRootRequest) Execute() (*DriveItem, *http.Response, error) { return r.ApiService.GetRootExecute(r) } @@ -600,6 +607,9 @@ func (a *DrivesRootApiService) GetRootExecute(r ApiGetRootRequest) (*DriveItem, if r.select_ != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "$select", r.select_, "form", "csv") } + if r.expand != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "$expand", r.expand, "form", "csv") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_me_drive_root.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_me_drive_root.go index bb435a2b46..b2885f0e7a 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_me_drive_root.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_me_drive_root.go @@ -26,6 +26,7 @@ type ApiHomeGetRootRequest struct { ctx context.Context ApiService *MeDriveRootApiService select_ *[]string + expand *[]string } // Select additional properties to be returned. @@ -34,6 +35,12 @@ func (r ApiHomeGetRootRequest) Select_(select_ []string) ApiHomeGetRootRequest { return r } +// Expand related entities to be returned. +func (r ApiHomeGetRootRequest) Expand(expand []string) ApiHomeGetRootRequest { + r.expand = &expand + return r +} + func (r ApiHomeGetRootRequest) Execute() (*DriveItem, *http.Response, error) { return r.ApiService.HomeGetRootExecute(r) } @@ -75,6 +82,9 @@ func (a *MeDriveRootApiService) HomeGetRootExecute(r ApiHomeGetRootRequest) (*Dr if r.select_ != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "$select", r.select_, "form", "csv") } + if r.expand != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "$expand", r.expand, "form", "csv") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go index a9a901a0c0..07e36220fc 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go @@ -42,6 +42,7 @@ type DriveItem struct { // An eTag for the content of the item. This eTag is not changed if only the metadata is changed. Note This property is not returned if the item is a folder. Read-only. CTag *string `json:"cTag,omitempty"` Deleted *Deleted `json:"deleted,omitempty"` + PendingOperations *PendingOperations `json:"pendingOperations,omitempty"` File *OpenGraphFile `json:"file,omitempty"` FileSystemInfo *FileSystemInfo `json:"fileSystemInfo,omitempty"` Folder *Folder `json:"folder,omitempty"` @@ -79,6 +80,8 @@ type DriveItem struct { LibreGraphTags []string `json:"@libre.graph.tags,omitempty"` // A list of actions the caller is allowed to perform on this item. Only returned when explicitly requested via `$select` on endpoints that support it. Mirrors the annotation of the same name on the `/permissions` endpoint, allowing clients to learn a caller's effective actions on an item without a separate round-trip. LibreGraphPermissionsActionsAllowedValues []string `json:"@libre.graph.permissions.actions.allowedValues,omitempty"` + // The types of shares existing on this item, aggregated over all of its grants. Absent or empty if the item is not shared. This is a summary of the item's `permissions` collection. For the full grants use the permissions endpoints, for the caller's own capabilities use `@libre.graph.permissions.actions.allowedValues`. Only returned when explicitly requested via `$select`. + LibreGraphShareTypes []string `json:"@libre.graph.shareTypes,omitempty"` } // NewDriveItem instantiates a new DriveItem object @@ -514,6 +517,38 @@ func (o *DriveItem) SetDeleted(v Deleted) { o.Deleted = &v } +// GetPendingOperations returns the PendingOperations field value if set, zero value otherwise. +func (o *DriveItem) GetPendingOperations() PendingOperations { + if o == nil || IsNil(o.PendingOperations) { + var ret PendingOperations + return ret + } + return *o.PendingOperations +} + +// GetPendingOperationsOk returns a tuple with the PendingOperations field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveItem) GetPendingOperationsOk() (*PendingOperations, bool) { + if o == nil || IsNil(o.PendingOperations) { + return nil, false + } + return o.PendingOperations, true +} + +// HasPendingOperations returns a boolean if a field has been set. +func (o *DriveItem) HasPendingOperations() bool { + if o != nil && !IsNil(o.PendingOperations) { + return true + } + + return false +} + +// SetPendingOperations gets a reference to the given PendingOperations and assigns it to the PendingOperations field. +func (o *DriveItem) SetPendingOperations(v PendingOperations) { + o.PendingOperations = &v +} + // GetFile returns the File field value if set, zero value otherwise. func (o *DriveItem) GetFile() OpenGraphFile { if o == nil || IsNil(o.File) { @@ -1314,6 +1349,38 @@ func (o *DriveItem) SetLibreGraphPermissionsActionsAllowedValues(v []string) { o.LibreGraphPermissionsActionsAllowedValues = v } +// GetLibreGraphShareTypes returns the LibreGraphShareTypes field value if set, zero value otherwise. +func (o *DriveItem) GetLibreGraphShareTypes() []string { + if o == nil || IsNil(o.LibreGraphShareTypes) { + var ret []string + return ret + } + return o.LibreGraphShareTypes +} + +// GetLibreGraphShareTypesOk returns a tuple with the LibreGraphShareTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveItem) GetLibreGraphShareTypesOk() ([]string, bool) { + if o == nil || IsNil(o.LibreGraphShareTypes) { + return nil, false + } + return o.LibreGraphShareTypes, true +} + +// HasLibreGraphShareTypes returns a boolean if a field has been set. +func (o *DriveItem) HasLibreGraphShareTypes() bool { + if o != nil && !IsNil(o.LibreGraphShareTypes) { + return true + } + + return false +} + +// SetLibreGraphShareTypes gets a reference to the given []string and assigns it to the LibreGraphShareTypes field. +func (o *DriveItem) SetLibreGraphShareTypes(v []string) { + o.LibreGraphShareTypes = v +} + func (o DriveItem) MarshalJSON() ([]byte, error) { toSerialize,err := o.ToMap() if err != nil { @@ -1363,6 +1430,9 @@ func (o DriveItem) ToMap() (map[string]interface{}, error) { if !IsNil(o.Deleted) { toSerialize["deleted"] = o.Deleted } + if !IsNil(o.PendingOperations) { + toSerialize["pendingOperations"] = o.PendingOperations + } if !IsNil(o.File) { toSerialize["file"] = o.File } @@ -1438,6 +1508,9 @@ func (o DriveItem) ToMap() (map[string]interface{}, error) { if !IsNil(o.LibreGraphPermissionsActionsAllowedValues) { toSerialize["@libre.graph.permissions.actions.allowedValues"] = o.LibreGraphPermissionsActionsAllowedValues } + if !IsNil(o.LibreGraphShareTypes) { + toSerialize["@libre.graph.shareTypes"] = o.LibreGraphShareTypes + } return toSerialize, nil } diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_recipient.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_recipient.go index 7692e064e3..ed620323e0 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_recipient.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_recipient.go @@ -19,6 +19,8 @@ var _ MappedNullable = &DriveRecipient{} // DriveRecipient Represents a person, group, or other recipient to share a drive item with using the invite action. When using invite to add permissions, the `driveRecipient` object would specify the `email`, `alias`, or `objectId` of the recipient. Only one of these values is required; multiple values are not accepted. type DriveRecipient struct { + // The email address for the recipient, if the recipient has an associated email address. + Email *string `json:"email,omitempty"` // The unique identifier for the recipient in the directory. ObjectId *string `json:"objectId,omitempty"` // When the recipient is referenced by objectId this annotation is used to differentiate `user` and `group` recipients. @@ -46,6 +48,38 @@ func NewDriveRecipientWithDefaults() *DriveRecipient { return &this } +// GetEmail returns the Email field value if set, zero value otherwise. +func (o *DriveRecipient) GetEmail() string { + if o == nil || IsNil(o.Email) { + var ret string + return ret + } + return *o.Email +} + +// GetEmailOk returns a tuple with the Email field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveRecipient) GetEmailOk() (*string, bool) { + if o == nil || IsNil(o.Email) { + return nil, false + } + return o.Email, true +} + +// HasEmail returns a boolean if a field has been set. +func (o *DriveRecipient) HasEmail() bool { + if o != nil && !IsNil(o.Email) { + return true + } + + return false +} + +// SetEmail gets a reference to the given string and assigns it to the Email field. +func (o *DriveRecipient) SetEmail(v string) { + o.Email = &v +} + // GetObjectId returns the ObjectId field value if set, zero value otherwise. func (o *DriveRecipient) GetObjectId() string { if o == nil || IsNil(o.ObjectId) { @@ -120,6 +154,9 @@ func (o DriveRecipient) MarshalJSON() ([]byte, error) { func (o DriveRecipient) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} + if !IsNil(o.Email) { + toSerialize["email"] = o.Email + } if !IsNil(o.ObjectId) { toSerialize["objectId"] = o.ObjectId } diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations.go new file mode 100644 index 0000000000..87f2c85dbb --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations.go @@ -0,0 +1,126 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the PendingOperations type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PendingOperations{} + +// PendingOperations Present while operations affecting the item's content have not completed, whether still queued or already running. While present, requests for the item's content fail, the content is withheld until processing completes and the facet disappears. +type PendingOperations struct { + PendingContentUpdate *PendingOperationsPendingContentUpdate `json:"pendingContentUpdate,omitempty"` +} + +// NewPendingOperations instantiates a new PendingOperations object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewPendingOperations() *PendingOperations { + this := PendingOperations{} + return &this +} + +// NewPendingOperationsWithDefaults instantiates a new PendingOperations object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewPendingOperationsWithDefaults() *PendingOperations { + this := PendingOperations{} + return &this +} + +// GetPendingContentUpdate returns the PendingContentUpdate field value if set, zero value otherwise. +func (o *PendingOperations) GetPendingContentUpdate() PendingOperationsPendingContentUpdate { + if o == nil || IsNil(o.PendingContentUpdate) { + var ret PendingOperationsPendingContentUpdate + return ret + } + return *o.PendingContentUpdate +} + +// GetPendingContentUpdateOk returns a tuple with the PendingContentUpdate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PendingOperations) GetPendingContentUpdateOk() (*PendingOperationsPendingContentUpdate, bool) { + if o == nil || IsNil(o.PendingContentUpdate) { + return nil, false + } + return o.PendingContentUpdate, true +} + +// HasPendingContentUpdate returns a boolean if a field has been set. +func (o *PendingOperations) HasPendingContentUpdate() bool { + if o != nil && !IsNil(o.PendingContentUpdate) { + return true + } + + return false +} + +// SetPendingContentUpdate gets a reference to the given PendingOperationsPendingContentUpdate and assigns it to the PendingContentUpdate field. +func (o *PendingOperations) SetPendingContentUpdate(v PendingOperationsPendingContentUpdate) { + o.PendingContentUpdate = &v +} + +func (o PendingOperations) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PendingOperations) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.PendingContentUpdate) { + toSerialize["pendingContentUpdate"] = o.PendingContentUpdate + } + return toSerialize, nil +} + +type NullablePendingOperations struct { + value *PendingOperations + isSet bool +} + +func (v NullablePendingOperations) Get() *PendingOperations { + return v.value +} + +func (v *NullablePendingOperations) Set(val *PendingOperations) { + v.value = val + v.isSet = true +} + +func (v NullablePendingOperations) IsSet() bool { + return v.isSet +} + +func (v *NullablePendingOperations) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePendingOperations(val *PendingOperations) *NullablePendingOperations { + return &NullablePendingOperations{value: val, isSet: true} +} + +func (v NullablePendingOperations) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePendingOperations) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations_pending_content_update.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations_pending_content_update.go new file mode 100644 index 0000000000..87df076384 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_pending_operations_pending_content_update.go @@ -0,0 +1,128 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "time" +) + +// checks if the PendingOperationsPendingContentUpdate type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &PendingOperationsPendingContentUpdate{} + +// PendingOperationsPendingContentUpdate An update to the item's content has not completed, for example post-processing such as virus scanning after an upload. MS Graph does not specify how reads behave while this is present; in OpenCloud content requests fail. +type PendingOperationsPendingContentUpdate struct { + // Time the operation was queued. May be absent. Read-only. + QueuedDateTime *time.Time `json:"queuedDateTime,omitempty"` +} + +// NewPendingOperationsPendingContentUpdate instantiates a new PendingOperationsPendingContentUpdate object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewPendingOperationsPendingContentUpdate() *PendingOperationsPendingContentUpdate { + this := PendingOperationsPendingContentUpdate{} + return &this +} + +// NewPendingOperationsPendingContentUpdateWithDefaults instantiates a new PendingOperationsPendingContentUpdate object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewPendingOperationsPendingContentUpdateWithDefaults() *PendingOperationsPendingContentUpdate { + this := PendingOperationsPendingContentUpdate{} + return &this +} + +// GetQueuedDateTime returns the QueuedDateTime field value if set, zero value otherwise. +func (o *PendingOperationsPendingContentUpdate) GetQueuedDateTime() time.Time { + if o == nil || IsNil(o.QueuedDateTime) { + var ret time.Time + return ret + } + return *o.QueuedDateTime +} + +// GetQueuedDateTimeOk returns a tuple with the QueuedDateTime field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PendingOperationsPendingContentUpdate) GetQueuedDateTimeOk() (*time.Time, bool) { + if o == nil || IsNil(o.QueuedDateTime) { + return nil, false + } + return o.QueuedDateTime, true +} + +// HasQueuedDateTime returns a boolean if a field has been set. +func (o *PendingOperationsPendingContentUpdate) HasQueuedDateTime() bool { + if o != nil && !IsNil(o.QueuedDateTime) { + return true + } + + return false +} + +// SetQueuedDateTime gets a reference to the given time.Time and assigns it to the QueuedDateTime field. +func (o *PendingOperationsPendingContentUpdate) SetQueuedDateTime(v time.Time) { + o.QueuedDateTime = &v +} + +func (o PendingOperationsPendingContentUpdate) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o PendingOperationsPendingContentUpdate) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.QueuedDateTime) { + toSerialize["queuedDateTime"] = o.QueuedDateTime + } + return toSerialize, nil +} + +type NullablePendingOperationsPendingContentUpdate struct { + value *PendingOperationsPendingContentUpdate + isSet bool +} + +func (v NullablePendingOperationsPendingContentUpdate) Get() *PendingOperationsPendingContentUpdate { + return v.value +} + +func (v *NullablePendingOperationsPendingContentUpdate) Set(val *PendingOperationsPendingContentUpdate) { + v.value = val + v.isSet = true +} + +func (v NullablePendingOperationsPendingContentUpdate) IsSet() bool { + return v.isSet +} + +func (v *NullablePendingOperationsPendingContentUpdate) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePendingOperationsPendingContentUpdate(val *PendingOperationsPendingContentUpdate) *NullablePendingOperationsPendingContentUpdate { + return &NullablePendingOperationsPendingContentUpdate{value: val, isSet: true} +} + +func (v NullablePendingOperationsPendingContentUpdate) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePendingOperationsPendingContentUpdate) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/modules.txt b/vendor/modules.txt index c098b7652a..afe5dd8368 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1364,7 +1364,7 @@ github.com/open-policy-agent/opa/v1/version # github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 ## explicit; go 1.24.6 github.com/opencloud-eu/icap-client -# 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 ## explicit; go 1.23 github.com/opencloud-eu/libre-graph-api-go # github.com/opencloud-eu/reva/v2 v2.49.0 From 68a9adcd3a058a804f03992ff0ebf2f5cf9d5348 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 2 Sep 2026 15:23:18 +0200 Subject: [PATCH 5/6] added new permission for guest invites --- .../service/v0/api_driveitem_permissions.go | 12 +++++++++++ .../v0/api_driveitem_permissions_test.go | 21 +++++++++++++++++++ .../settings/pkg/store/defaults/defaults.go | 4 ++++ .../pkg/store/defaults/permissions.go | 19 +++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 3949a8e35b..537b15bc78 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -15,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" @@ -168,6 +170,16 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto if !s.config.EnableGuestInvites { return libregraph.Permission{}, errorcode.New(errorcode.NotSupported, "sharing with mail recipients is not enabled") } + 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") diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index 71887cd4c1..322ebbc1d3 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -13,6 +13,8 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + 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" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -169,6 +171,8 @@ var _ = Describe("DriveItemPermissionsService", func() { It("creates guest share using an email address", func() { cfg.EnableGuestInvites = true + gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return( + &permissionsapi.CheckPermissionResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, nil) gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ @@ -187,6 +191,8 @@ var _ = Describe("DriveItemPermissionsService", func() { }) It("verifies that invalid email addresses are handled", func() { cfg.EnableGuestInvites = true + gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return( + &permissionsapi.CheckPermissionResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ {Email: libregraph.PtrString("invalid")}, } @@ -198,6 +204,8 @@ var _ = Describe("DriveItemPermissionsService", func() { It("verifies that empty email addresses are handled", func() { cfg.EnableGuestInvites = true + gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return( + &permissionsapi.CheckPermissionResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ {Email: libregraph.PtrString(" ")}, } @@ -218,6 +226,19 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(err.Error()).To(ContainSubstring("not enabled")) }) + It("rejects guest shares without the permission to invite guests", func() { + cfg.EnableGuestInvites = true + gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return( + &permissionsapi.CheckPermissionResponse{Status: &rpc.Status{Code: rpc.Code_CODE_PERMISSION_DENIED}}, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {Email: libregraph.PtrString("guest@example.com")}, + } + _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("permission denied")) + }) + It("succeeds with file roles (happy path)", func() { gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go index c8d144eefd..f096d8a659 100644 --- a/services/settings/pkg/store/defaults/defaults.go +++ b/services/settings/pkg/store/defaults/defaults.go @@ -86,6 +86,7 @@ func ServiceAccountBundle() *settingsmsg.Bundle { CollaborationManageFontsPermission(All), CreatePublicLinkPermission(All), CreateSharePermission(All), + CreateGuestInvitePermission(All), CreateSpacesPermission(All), DeletePersonalSpacesPermission(All), DeleteProjectSpacesPermission(All), @@ -125,6 +126,7 @@ func generateBundleAdminRole() *settingsmsg.Bundle { CollaborationManageFontsPermission(All), CreatePublicLinkPermission(All), CreateSharePermission(All), + CreateGuestInvitePermission(All), CreateSpacesPermission(All), DeletePersonalSpacesPermission(All), DeleteProjectSpacesPermission(All), @@ -170,6 +172,7 @@ func generateBundleSpaceAdminRole() *settingsmsg.Bundle { AutoAcceptSharesPermission(Own), CreatePublicLinkPermission(All), CreateSharePermission(All), + CreateGuestInvitePermission(All), CreateSpacesPermission(All), DeleteProjectSpacesPermission(All), DeleteReadOnlyPublicLinkPasswordPermission(All), @@ -211,6 +214,7 @@ func generateBundleUserRole() *settingsmsg.Bundle { AutoAcceptSharesPermission(Own), CreatePublicLinkPermission(All), CreateSharePermission(All), + CreateGuestInvitePermission(All), CreateSpacesPermission(Own), DisableEmailNotificationsPermission(Own), ProfileEmailSendingIntervalPermission(Own), diff --git a/services/settings/pkg/store/defaults/permissions.go b/services/settings/pkg/store/defaults/permissions.go index 7d6ee14de6..3ee8ee3789 100644 --- a/services/settings/pkg/store/defaults/permissions.go +++ b/services/settings/pkg/store/defaults/permissions.go @@ -162,6 +162,25 @@ func CreateSharePermission(c settingsmsg.Permission_Constraint) *settingsmsg.Set } } +// CreateGuestInvitePermission is the permission to create guest (mail) invites. +func CreateGuestInvitePermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting { + return &settingsmsg.Setting{ + Id: "54ca22e9-8b30-4826-b9b6-284b62b69289", + Name: "GuestInvites.Create", + DisplayName: "Invite guests by email", + Description: "This permission allows creating guest (mail) invites.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SHARE, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_WRITE, + Constraint: c, + }, + }, + } +} + // CreateSpacesPermission is the permission to create spaces func CreateSpacesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting { return &settingsmsg.Setting{ From 0984c08d53de31f778767496b4e9f8205d47cf52 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 3 Sep 2026 11:34:12 +0200 Subject: [PATCH 6/6] upd graph api drive recipient validation --- services/graph/pkg/validate/libregraph.go | 16 +------ .../graph/pkg/validate/libregraph_test.go | 45 +++++++++++++------ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go index 9932ede0a5..266c261f73 100644 --- a/services/graph/pkg/validate/libregraph.go +++ b/services/graph/pkg/validate/libregraph.go @@ -46,21 +46,9 @@ func libregraphDriveItemInvite(v *validator.Validate) { // libregraphDriveRecipient validates libregraph.DriveRecipient func libregraphDriveRecipient(v *validator.Validate) { v.RegisterStructValidationMapRules(map[string]string{ - "ObjectId": "omitempty,ne=", + "ObjectId": "required_without=Email,omitempty,ne=,excluded_with=Email", "Email": "omitempty,email", - "LibreGraphRecipientType": "oneof=user group", - }, libregraph.DriveRecipient{}) - - v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) { - driveRecipient := sl.Current().Interface().(libregraph.DriveRecipient) - - if driveRecipient.GetObjectId() == "" && driveRecipient.GetEmail() == "" { - sl.ReportError(driveRecipient.ObjectId, "ObjectId", "objectId", "oneof", "either objectId or email is required") - return - } - if driveRecipient.GetObjectId() != "" && driveRecipient.GetEmail() != "" { - sl.ReportError(driveRecipient.ObjectId, "ObjectId", "objectId", "exclusive", "objectId and email are mutually exclusive") - } + "LibreGraphRecipientType": "required_with=ObjectId,excluded_with=Email,omitempty,oneof=user group", }, libregraph.DriveRecipient{}) } diff --git a/services/graph/pkg/validate/libregraph_test.go b/services/graph/pkg/validate/libregraph_test.go index 32992022ca..01d7020a08 100644 --- a/services/graph/pkg/validate/libregraph_test.go +++ b/services/graph/pkg/validate/libregraph_test.go @@ -137,27 +137,37 @@ var _ = Describe("libregraph", func() { } } }, - Entry("fail: invalid objectId", + Entry("succeed: user recipient", func() (libregraph.DriveRecipient, bool) { - driveRecipient.ObjectId = nil - return driveRecipient, false + driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user") + return driveRecipient, true }, + ), + Entry("succeed: group recipient", func() (libregraph.DriveRecipient, bool) { - driveRecipient.ObjectId = conversions.ToPointer("") - return driveRecipient, false + driveRecipient.LibreGraphRecipientType = conversions.ToPointer("group") + return driveRecipient, true }, ), Entry("succeed: email recipient", func() (libregraph.DriveRecipient, bool) { driveRecipient.ObjectId = nil driveRecipient.Email = conversions.ToPointer("guest@example.com") + driveRecipient.LibreGraphRecipientType = nil return driveRecipient, true }, ), - Entry("fail: invalid email", + Entry("fail: no objectId and no email", func() (libregraph.DriveRecipient, bool) { driveRecipient.ObjectId = nil - driveRecipient.Email = conversions.ToPointer("invalid") + driveRecipient.Email = nil + driveRecipient.LibreGraphRecipientType = nil + return driveRecipient, false + }, + ), + Entry("fail: empty objectId", + func() (libregraph.DriveRecipient, bool) { + driveRecipient.ObjectId = conversions.ToPointer("") return driveRecipient, false }, ), @@ -167,22 +177,29 @@ var _ = Describe("libregraph", func() { return driveRecipient, false }, ), - Entry("succeed: valid role", + Entry("fail: objectId without recipient type", func() (libregraph.DriveRecipient, bool) { - driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user") - return driveRecipient, true + driveRecipient.LibreGraphRecipientType = nil + return driveRecipient, false }, + ), + Entry("fail: objectId with invalid recipient type", func() (libregraph.DriveRecipient, bool) { - driveRecipient.LibreGraphRecipientType = conversions.ToPointer("group") - return driveRecipient, true + driveRecipient.LibreGraphRecipientType = conversions.ToPointer("foo") + return driveRecipient, false }, ), - Entry("fail: invalid role", + Entry("fail: email recipient with recipient type set", func() (libregraph.DriveRecipient, bool) { - driveRecipient.LibreGraphRecipientType = conversions.ToPointer("foo") + driveRecipient.ObjectId = nil + driveRecipient.Email = conversions.ToPointer("guest@example.com") return driveRecipient, false }, + ), + Entry("fail: invalid email", func() (libregraph.DriveRecipient, bool) { + driveRecipient.ObjectId = nil + driveRecipient.Email = conversions.ToPointer("invalid") driveRecipient.LibreGraphRecipientType = nil return driveRecipient, false },