diff --git a/go.mod b/go.mod index 80444f78c4..37da7db945 100644 --- a/go.mod +++ b/go.mod @@ -56,14 +56,15 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/nats-io/nats-server/v2 v2.14.5 github.com/nats-io/nats.go v1.53.1 + github.com/olekukonko/errors v1.2.0 github.com/olekukonko/tablewriter v1.1.4 github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.32.1 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.20260902170011-45af3945a067 - github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774 + github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260908131234-71ed393d5010 + github.com/opencloud-eu/reva/v2 v2.50.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 @@ -308,7 +309,6 @@ require ( github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.2.0 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.6 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect diff --git a/go.sum b/go.sum index 82de5825e3..96f4905330 100644 --- a/go.sum +++ b/go.sum @@ -940,10 +940,10 @@ 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.20260902170011-45af3945a067 h1:UkNMKauyJAzY6RE6mmthz9bQZLYkbvBuApm7ZDCparE= -github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= -github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774 h1:8N42Pwzyafdxq66zYP6kJ1CH/QBRpxovQXq/0Kt6bJY= -github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260908131234-71ed393d5010 h1:DOQa8d1MFZ2vxDoJ4TQKf9gV5ivyCT/LGEe1BzkITNc= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260908131234-71ed393d5010/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= +github.com/opencloud-eu/reva/v2 v2.50.0 h1:N5e0AuqACiZdQZAlEk7JqrSQfqwQVt26xAsCGzzOxOo= +github.com/opencloud-eu/reva/v2 v2.50.0/go.mod h1:PZFZK90dVfh+pkelCbgtGjmUQots9x7JOd1I2tt2aIQ= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 3b74eab6e7..43b92ce927 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 63d9ec49af..537b15bc78 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" @@ -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" @@ -163,24 +166,42 @@ 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 @@ -188,60 +209,99 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto 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() + } + } } @@ -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() 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..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" @@ -167,6 +169,76 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(permission.GrantedToV2.Group.GetId()).To(Equal("2")) }) + 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{ + {Email: libregraph.PtrString("Test User ")}, + } + 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() { + 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")}, + } + + _, 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() { + 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(" ")}, + } + + _, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid mail recipient")) + }) + + It("rejects guest shares when guest invites are disabled by default", func() { + 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("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) @@ -326,6 +398,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/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 5927e812e1..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,28 +110,42 @@ 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) { + lgIdentity := libregraph.Identity{ + Id: libregraph.PtrString(cs3UserID.GetOpaqueId()), + LibreGraphUserType: libregraph.PtrString(identity.UserTypeGuest), + } + 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 // 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/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 diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go index 572427b6b1..266c261f73 100644 --- a/services/graph/pkg/validate/libregraph.go +++ b/services/graph/pkg/validate/libregraph.go @@ -46,8 +46,9 @@ 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", + "ObjectId": "required_without=Email,omitempty,ne=,excluded_with=Email", + "Email": "omitempty,email", + "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 d7f189764a..01d7020a08 100644 --- a/services/graph/pkg/validate/libregraph_test.go +++ b/services/graph/pkg/validate/libregraph_test.go @@ -137,32 +137,69 @@ var _ = Describe("libregraph", func() { } } }, - Entry("fail: invalid objectId", + Entry("succeed: user recipient", + func() (libregraph.DriveRecipient, bool) { + driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user") + return driveRecipient, true + }, + ), + Entry("succeed: group recipient", + func() (libregraph.DriveRecipient, bool) { + 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: no objectId and no email", func() (libregraph.DriveRecipient, bool) { driveRecipient.ObjectId = nil + driveRecipient.Email = nil + driveRecipient.LibreGraphRecipientType = nil return driveRecipient, false }, + ), + Entry("fail: empty objectId", func() (libregraph.DriveRecipient, bool) { driveRecipient.ObjectId = conversions.ToPointer("") return driveRecipient, false }, ), - Entry("succeed: valid role", + Entry("fail: objectId and email both set", func() (libregraph.DriveRecipient, bool) { - driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user") - return driveRecipient, true + driveRecipient.Email = conversions.ToPointer("guest@example.com") + return driveRecipient, false }, + ), + Entry("fail: objectId without recipient type", func() (libregraph.DriveRecipient, bool) { - driveRecipient.LibreGraphRecipientType = conversions.ToPointer("group") - return driveRecipient, true + driveRecipient.LibreGraphRecipientType = nil + return driveRecipient, false }, ), - Entry("fail: invalid role", + Entry("fail: objectId with invalid recipient type", func() (libregraph.DriveRecipient, bool) { driveRecipient.LibreGraphRecipientType = conversions.ToPointer("foo") return driveRecipient, false }, + ), + Entry("fail: email recipient with recipient type set", + func() (libregraph.DriveRecipient, bool) { + 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 }, diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go index 5f3d9e0fa1..67f09f58db 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{ 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 286f9658e2..d53276a5df 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 @@ -374,6 +374,13 @@ GetDriveItem Get a DriveItem. Get a DriveItem by using its ID. +This endpoint also accepts the MS Graph colon-syntax URL forms: + + GET /v1beta1/drives/{drive-id}/root:/{path} + GET /v1beta1/drives/{drive-id}/items/{item-id}:/{path} + +OpenAPI cannot express the colon-delimited path segment, so these URL forms are not represented as separate operations in this specification. The server still accepts them and resolves `:/{path}` as the item to return. A trailing `:` terminator is accepted as well. + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). @param driveId key: id of drive @@ -792,6 +799,13 @@ Get a DriveItem by using its ID. Modeled on the MS Graph get driveItem endpoint (https://learn.microsoft.com/en-us/graph/api/driveitem-get). +This endpoint also accepts the MS Graph colon-syntax URL forms: + + GET /v1.0/drives/{drive-id}/root:/{path} + GET /v1.0/drives/{drive-id}/items/{item-id}:/{path} + +OpenAPI cannot express the colon-delimited path segment, so these URL forms are not represented as separate operations in this specification. The server still accepts them and resolves `:/{path}` as the item to return. A trailing `:` terminator is accepted as well. + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). @param driveId key: id of drive diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go index c2f3f9db6c..466a500973 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go @@ -20,6 +20,7 @@ package usershareprovider import ( "context" + "net/mail" "path/filepath" "regexp" "slices" @@ -180,8 +181,18 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar Status: status.NewPermissionDenied(ctx, nil, "user tenantId does not match the target user tenantId"), }, nil } - } + if req.GetGrant().GetGrantee().GetUserId().GetType() == userpb.UserType_USER_TYPE_GUEST { + // guests are identified by their mail address, lets be strict here and only accept bare mail addresses + // and reject the "Mailbox"-Format that also contains a Display name + addr, err := mail.ParseAddress(req.GetGrant().GetGrantee().GetUserId().GetOpaqueId()) + if err != nil || addr.Name != "" { + return &collaboration.CreateShareResponse{ + Status: status.NewInvalidArg(ctx, "invalid mail address for guest grantee"), + }, nil + } + } + } gatewayClient, err := s.gatewaySelector.Next() if err != nil { return nil, err @@ -206,7 +217,7 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar UserId: &userpb.UserId{ OpaqueId: req.GetGrant().GetGrantee().GetUserId().GetOpaqueId(), Idp: user.GetId().GetIdp(), - Type: userpb.UserType_USER_TYPE_PRIMARY}, + Type: req.GetGrant().GetGrantee().GetUserId().GetType()}, } } // some for group grantees diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/appauth/manager/jsoncs3/jsoncs3.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/appauth/manager/jsoncs3/jsoncs3.go index f15852b2bf..98244e5182 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/appauth/manager/jsoncs3/jsoncs3.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/appauth/manager/jsoncs3/jsoncs3.go @@ -2,6 +2,8 @@ package jsoncs3 import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" "strings" "sync" @@ -14,6 +16,7 @@ import ( typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" + "github.com/hashicorp/golang-lru/v2/expirable" "github.com/opencloud-eu/reva/v2/pkg/appauth" "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry" "github.com/opencloud-eu/reva/v2/pkg/appctx" @@ -43,6 +46,7 @@ type manager struct { store *metadatacache.Store[string, map[string]*apppb.AppPassword] generator PasswordGenerator uTimeUpdateInterval time.Duration + authCache *expirable.LRU[string, *apppb.AppPassword] initialized bool } @@ -59,7 +63,10 @@ type config struct { UpdateRetryCount int `mapstructure:"update_retry_count"` } -const tracerName = "jsoncs3" +const ( + tracerName = "jsoncs3" + defaultCacheTTL = 60 * time.Second +) func New(m map[string]any) (appauth.Manager, error) { c := &config{} @@ -131,11 +138,13 @@ func NewWithOptions(mds metadata.Storage, generator PasswordGenerator, uTimeUpda Retries: updateRetries, Init: func() map[string]*apppb.AppPassword { return map[string]*apppb.AppPassword{} }, }) + return &manager{ mds: mds, store: store, generator: generator, uTimeUpdateInterval: uTimeUpdateInterval, + authCache: expirable.NewLRU[string, *apppb.AppPassword](0, nil, defaultCacheTTL), }, nil } @@ -291,6 +300,8 @@ func (m *manager) InvalidateAppPassword(ctx context.Context, secretOrId string) log.Error().Err(err).Msg("store.Update failed") return errtypes.NotFound("password not found") } + + m.removeFromAuthCache(userID.GetOpaqueId(), secretOrId) return nil } @@ -310,6 +321,19 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre matchedID string ) + // check for a previously validated authentication result from memory first, to avoid + // recomputing the Argon2id hash for every stored password. + cacheKey := createAuthCacheKey(user.GetOpaqueId(), secret) + if cached, ok := m.authCache.Get(cacheKey); ok { + if isAppPasswordExpired(cached) { + m.authCache.Remove(cacheKey) + return nil, errtypes.NotFound("password not found") + } + result := proto.Clone(cached).(*apppb.AppPassword) + result.Password = cached.Password + return result, nil + } + err := m.store.Update(ctx, user.GetOpaqueId(), false, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, bool, error) { matchedPw = nil for id, pw := range a { @@ -319,7 +343,7 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre log.Debug().Err(err).Msg("Error comparing password and hash") case ok: // password found - if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds { + if isAppPasswordExpired(pw) { log.Debug().Str("AppPasswordId", id).Msg("password expired") return nil, false, errtypes.NotFound("password not found") } @@ -328,8 +352,13 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre matchedID = id // Updating the Utime will cause an Upload for every single GetAppPassword request. We are limiting this to one // update per 'uTimeUpdateInterval' (default 5 min) otherwise this backend will become unusable. + persist := false if time.Since(utils.TSToTime(pw.Utime)) > m.uTimeUpdateInterval { a[id].Utime = utils.TSNow() + persist = true + } + + if persist { return a, true, nil } return a, false, nil @@ -345,9 +374,31 @@ func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secre // is not corrupted. result := proto.Clone(matchedPw).(*apppb.AppPassword) result.Password = matchedID + m.authCache.Add(cacheKey, result) + return result, nil } +// removeFromAuthCache removes the cached entry matching the app password. +// secretOrIdis actually a password which is coming to the InvalidateAppPassword method +// and then is propagated here, just keeped the same naming +func (m *manager) removeFromAuthCache(userID, secretOrId string) { + key := createAuthCacheKey(userID, secretOrId) + if m.authCache.Remove(key) { + return + } + for _, k := range m.authCache.Keys() { + v, ok := m.authCache.Peek(k) + if ok && v.Password == secretOrId { + m.authCache.Remove(k) + } + } +} + +func isAppPasswordExpired(pw *apppb.AppPassword) bool { + return pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds +} + func (m *manager) initialize(ctx context.Context) error { _, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize") logger := appctx.GetLogger(ctx) @@ -424,3 +475,11 @@ func (d dicewarePassword) GeneratePassword() (string, error) { } return strings.Join(token, " "), nil } + +func createAuthCacheKey(userID, secret string) string { + h := sha256.New() + _, _ = h.Write([]byte(userID)) + _, _ = h.Write([]byte{0}) + _, _ = h.Write([]byte(secret)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/mocks/Stream.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/mocks/Stream.go deleted file mode 100644 index 3279b30c38..0000000000 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/mocks/Stream.go +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2018-2022 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -// Code generated by mockery v2.53.2. DO NOT EDIT. - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" - events "go-micro.dev/v4/events" -) - -// Stream is an autogenerated mock type for the Stream type -type Stream struct { - mock.Mock -} - -type Stream_Expecter struct { - mock *mock.Mock -} - -func (_m *Stream) EXPECT() *Stream_Expecter { - return &Stream_Expecter{mock: &_m.Mock} -} - -// Consume provides a mock function with given fields: _a0, _a1 -func (_m *Stream) Consume(_a0 string, _a1 ...events.ConsumeOption) (<-chan events.Event, error) { - var tmpRet mock.Arguments - if len(_a1) > 0 { - tmpRet = _m.Called(_a0, _a1) - } else { - tmpRet = _m.Called(_a0) - } - ret := tmpRet - - if len(ret) == 0 { - panic("no return value specified for Consume") - } - - var r0 <-chan events.Event - var r1 error - if rf, ok := ret.Get(0).(func(string, ...events.ConsumeOption) (<-chan events.Event, error)); ok { - return rf(_a0, _a1...) - } - if rf, ok := ret.Get(0).(func(string, ...events.ConsumeOption) <-chan events.Event); ok { - r0 = rf(_a0, _a1...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan events.Event) - } - } - - if rf, ok := ret.Get(1).(func(string, ...events.ConsumeOption) error); ok { - r1 = rf(_a0, _a1...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Stream_Consume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Consume' -type Stream_Consume_Call struct { - *mock.Call -} - -// Consume is a helper method to define mock.On call -// - _a0 string -// - _a1 ...events.ConsumeOption -func (_e *Stream_Expecter) Consume(_a0 interface{}, _a1 ...interface{}) *Stream_Consume_Call { - return &Stream_Consume_Call{Call: _e.mock.On("Consume", - append([]interface{}{_a0}, _a1...)...)} -} - -func (_c *Stream_Consume_Call) Run(run func(_a0 string, _a1 ...events.ConsumeOption)) *Stream_Consume_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]events.ConsumeOption, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(events.ConsumeOption) - } - } - run(args[0].(string), variadicArgs...) - }) - return _c -} - -func (_c *Stream_Consume_Call) Return(_a0 <-chan events.Event, _a1 error) *Stream_Consume_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *Stream_Consume_Call) RunAndReturn(run func(string, ...events.ConsumeOption) (<-chan events.Event, error)) *Stream_Consume_Call { - _c.Call.Return(run) - return _c -} - -// Publish provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Stream) Publish(_a0 string, _a1 interface{}, _a2 ...events.PublishOption) error { - var tmpRet mock.Arguments - if len(_a2) > 0 { - tmpRet = _m.Called(_a0, _a1, _a2) - } else { - tmpRet = _m.Called(_a0, _a1) - } - ret := tmpRet - - if len(ret) == 0 { - panic("no return value specified for Publish") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, interface{}, ...events.PublishOption) error); ok { - r0 = rf(_a0, _a1, _a2...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Stream_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' -type Stream_Publish_Call struct { - *mock.Call -} - -// Publish is a helper method to define mock.On call -// - _a0 string -// - _a1 interface{} -// - _a2 ...events.PublishOption -func (_e *Stream_Expecter) Publish(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *Stream_Publish_Call { - return &Stream_Publish_Call{Call: _e.mock.On("Publish", - append([]interface{}{_a0, _a1}, _a2...)...)} -} - -func (_c *Stream_Publish_Call) Run(run func(_a0 string, _a1 interface{}, _a2 ...events.PublishOption)) *Stream_Publish_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]events.PublishOption, len(args)-2) - for i, a := range args[2:] { - if a != nil { - variadicArgs[i] = a.(events.PublishOption) - } - } - run(args[0].(string), args[1].(interface{}), variadicArgs...) - }) - return _c -} - -func (_c *Stream_Publish_Call) Return(_a0 error) *Stream_Publish_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Stream_Publish_Call) RunAndReturn(run func(string, interface{}, ...events.PublishOption) error) *Stream_Publish_Call { - _c.Call.Return(run) - return _c -} - -// NewStream creates a new instance of Stream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewStream(t interface { - mock.TestingT - Cleanup(func()) -}) *Stream { - mock := &Stream{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go index 021b541bd9..492ec38009 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go @@ -26,6 +26,7 @@ import ( "time" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -340,6 +341,12 @@ func (m *Manager) initialize(ctx context.Context) error { return nil } +// Ready returns a channel that is closed once the background initialization +// goroutine has successfully connected to the metadata storage. +func (m *Manager) Ready() <-chan struct{} { + return m.ready +} + // waitForInit blocks until the background initialization goroutine has // successfully completed, or until ctx is cancelled. func (m *Manager) waitForInit(ctx context.Context) error { @@ -476,7 +483,7 @@ func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *colla }) eg.Go(func() error { - err := m.CreatedCache.Add(ctx, s.GetCreator().GetOpaqueId(), shareID) + err := m.CreatedCache.Add(ctx, utils.NewFSSafeUserID(s.GetCreator()), shareID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -490,13 +497,12 @@ func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *colla switch g.Grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: eg.Go(func() error { - userid := g.Grantee.GetUserId().GetOpaqueId() rs := &collaboration.ReceivedShare{ Share: s, State: collaboration.ShareState_SHARE_STATE_PENDING, } - err := m.UserReceivedStates.Add(ctx, userid, spaceID, rs) + err := m.UserReceivedStates.Add(ctx, utils.NewFSSafeUserID(g.GetGrantee().GetUserId()), spaceID, rs) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -506,8 +512,7 @@ func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *colla }) case provider.GranteeType_GRANTEE_TYPE_GROUP: eg.Go(func() error { - groupid := g.Grantee.GetGroupId().GetOpaqueId() - err := m.GroupReceivedCache.Add(ctx, groupid, shareID) + err := m.GroupReceivedCache.Add(ctx, utils.FSSafeGroupID{ID: g.GetGrantee().GetGroupId()}, shareID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -873,7 +878,7 @@ func (m *Manager) listCreatedShares(ctx context.Context, user *userv1beta1.User, defer span.End() sublog := appctx.GetLogger(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Str("useridp", user.GetId().GetIdp()).Str("driver", "jsoncs3").Str("handler", "listCreatedShares").Logger() - list, err := m.CreatedCache.List(ctx, user.Id.OpaqueId) + list, err := m.CreatedCache.List(ctx, utils.NewFSSafeUserID(user.GetId())) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -1018,7 +1023,10 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati // first collect all spaceids the user has access to as a group member for _, group := range user.Groups { - list, err := m.GroupReceivedCache.List(ctx, group) + groupid := grouppb.GroupId{ + OpaqueId: group, + } + list, err := m.GroupReceivedCache.List(ctx, utils.FSSafeGroupID{ID: &groupid}) if err != nil { continue // ignore error, cache will be updated on next read } @@ -1043,7 +1051,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati } // add all spaces the user has receved shares for, this includes mount points and share state for groups - spaces, err := m.UserReceivedStates.List(ctx, user.Id.OpaqueId) + spaces, err := m.UserReceivedStates.List(ctx, utils.NewFSSafeUserID(user.GetId())) if err != nil { return nil, err } @@ -1106,7 +1114,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati } if s == nil { sublogr.Warn().Str("shareid", shareID).Msg("share not found. cleaning up") - _ = m.UserReceivedStates.Remove(ctx, user.Id.OpaqueId, w.ssid, shareID) + _ = m.UserReceivedStates.Remove(ctx, utils.NewFSSafeUserID(user.GetId()), w.ssid, shareID) continue } sublogr = sublogr.With().Str("shareid", shareID).Logger() @@ -1184,7 +1192,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati } // convert must be called in a lock-controlled block. -func (m *Manager) convert(ctx context.Context, userID string, s *collaboration.Share) *collaboration.ReceivedShare { +func (m *Manager) convert(ctx context.Context, userID *userv1beta1.UserId, s *collaboration.Share) *collaboration.ReceivedShare { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "convert") defer span.End() @@ -1195,7 +1203,7 @@ func (m *Manager) convert(ctx context.Context, userID string, s *collaboration.S storageID, spaceID, _ := shareid.Decode(s.Id.OpaqueId) - state, err := m.UserReceivedStates.Get(ctx, userID, storageID+shareid.IDDelimiter+spaceID, s.Id.GetOpaqueId()) + state, err := m.UserReceivedStates.Get(ctx, utils.NewFSSafeUserID(userID), storageID+shareid.IDDelimiter+spaceID, s.Id.GetOpaqueId()) if err == nil && state != nil { rs.State = state.State rs.MountPoint = state.MountPoint @@ -1255,7 +1263,7 @@ func (m *Manager) getReceived(ctx context.Context, ref *collaboration.ShareRefer } } } - return m.convert(ctx, user.Id.GetOpaqueId(), s), nil + return m.convert(ctx, user.GetId(), s), nil } // UpdateReceivedShare updates the received share with share state. @@ -1290,12 +1298,12 @@ func (m *Manager) UpdateReceivedShare(ctx context.Context, receivedShare *collab // write back u := ctxpkg.ContextMustGetUser(ctx) - uid := u.GetId().GetOpaqueId() + uid := u.GetId() if u.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE { - uid = forUser.GetOpaqueId() + uid = forUser } - err = m.UserReceivedStates.Add(ctx, uid, rs.Share.ResourceId.StorageId+shareid.IDDelimiter+rs.Share.ResourceId.SpaceId, rs) + err = m.UserReceivedStates.Add(ctx, utils.NewFSSafeUserID(uid), rs.Share.ResourceId.StorageId+shareid.IDDelimiter+rs.Share.ResourceId.SpaceId, rs) if err != nil { return nil, err } @@ -1333,7 +1341,7 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar } else { l.Debug().Str("storageid", s.GetResourceId().GetStorageId()).Str("spaceid", s.GetResourceId().GetSpaceId()).Str("shareid", s.Id.OpaqueId).Msg("imported share") } - if err := m.CreatedCache.Add(ctx, s.GetCreator().GetOpaqueId(), s.Id.OpaqueId); err != nil { + if err := m.CreatedCache.Add(ctx, utils.NewFSSafeUserID(s.GetCreator()), s.Id.OpaqueId); err != nil { l.Error().Err(err).Interface("share", s).Msg("error persisting created cache") } else { l.Debug().Str("creatorid", s.GetCreator().GetOpaqueId()).Str("shareid", s.Id.OpaqueId).Msg("updated created cache") @@ -1349,14 +1357,14 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar } if s.UserID != nil { spaceid := s.ReceivedShare.GetShare().GetResourceId().GetStorageId() + shareid.IDDelimiter + s.ReceivedShare.GetShare().GetResourceId().GetSpaceId() - if err := m.UserReceivedStates.Add(context.Background(), s.UserID.GetOpaqueId(), spaceid, s.ReceivedShare); err != nil { + if err := m.UserReceivedStates.Add(context.Background(), utils.NewFSSafeUserID(s.UserID), spaceid, s.ReceivedShare); err != nil { l.Error().Err(err).Interface("received share", s).Msg("error persisting received share for user") } else { l.Debug().Str("userid", s.UserID.GetOpaqueId()).Str("spaceid", spaceid).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share userdata") } } if s.ReceivedShare.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.UserID == nil { - if err := m.GroupReceivedCache.Add(context.Background(), s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId(), s.ReceivedShare.GetShare().GetId().GetOpaqueId()); err != nil { + if err := m.GroupReceivedCache.Add(context.Background(), utils.FSSafeGroupID{ID: s.ReceivedShare.GetShare().GetGrantee().GetGroupId()}, s.ReceivedShare.GetShare().GetId().GetOpaqueId()); err != nil { l.Error().Err(err).Interface("received share", s).Msg("error persisting received share to group cache") } else { l.Debug().Str("groupid", s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share group cache") @@ -1412,15 +1420,15 @@ func (m *Manager) removeShare(ctx context.Context, s *collaboration.Share, skipS eg.Go(func() error { // remove from created cache - return m.CreatedCache.Remove(ctx, s.GetCreator().GetOpaqueId(), s.Id.OpaqueId) + return m.CreatedCache.Remove(ctx, utils.NewFSSafeUserID(s.GetCreator()), s.Id.OpaqueId) }) eg.Go(func() error { // remove from user received states if s.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER { - return m.UserReceivedStates.Remove(ctx, s.GetGrantee().GetUserId().GetOpaqueId(), s.GetResourceId().GetStorageId()+shareid.IDDelimiter+s.GetResourceId().GetSpaceId(), s.Id.OpaqueId) + return m.UserReceivedStates.Remove(ctx, utils.NewFSSafeUserID(s.GetGrantee().GetUserId()), s.GetResourceId().GetStorageId()+shareid.IDDelimiter+s.GetResourceId().GetSpaceId(), s.Id.OpaqueId) } else if s.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - return m.GroupReceivedCache.Remove(ctx, s.GetGrantee().GetGroupId().GetOpaqueId(), s.Id.OpaqueId) + return m.GroupReceivedCache.Remove(ctx, utils.FSSafeGroupID{ID: s.GetGrantee().GetGroupId()}, s.Id.OpaqueId) } return nil }) diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache/receivedsharecache.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache/receivedsharecache.go index a0e94361a9..b8e536b082 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache/receivedsharecache.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache/receivedsharecache.go @@ -34,6 +34,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/errtypes" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/mtimesyncedcache" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + "github.com/opencloud-eu/reva/v2/pkg/utils" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" ) @@ -82,8 +83,8 @@ func New(s metadata.Storage, ttl time.Duration) Cache { } } -func (c *Cache) lockUser(userID string) func() { - v, _ := c.lockMap.LoadOrStore(userID, &sync.Mutex{}) +func (c *Cache) lockUser(userID utils.FilenameEncoder) func() { + v, _ := c.lockMap.LoadOrStore(userID.SafeFilename(), &sync.Mutex{}) lock := v.(*sync.Mutex) lock.Lock() @@ -91,14 +92,15 @@ func (c *Cache) lockUser(userID string) func() { } // Add adds a new entry to the cache -func (c *Cache) Add(ctx context.Context, userID, spaceID string, rs *collaboration.ReceivedShare) error { +func (c *Cache) Add(ctx context.Context, userID utils.FilenameEncoder, spaceID string, rs *collaboration.ReceivedShare) error { + userIDKey := userID.SafeFilename() ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") unlock := c.lockUser(userID) span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) defer unlock() - if _, ok := c.ReceivedSpaces.Load(userID); !ok { + if _, ok := c.ReceivedSpaces.Load(userIDKey); !ok { err := c.syncWithLock(ctx, userID) if err != nil { return err @@ -107,12 +109,12 @@ func (c *Cache) Add(ctx context.Context, userID, spaceID string, rs *collaborati ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userID), attribute.String("cs3.spaceid", spaceID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey), attribute.String("cs3.spaceid", spaceID)) persistFunc := func() error { - c.initializeIfNeeded(userID, spaceID) + c.initializeIfNeeded(userIDKey, spaceID) - rss, _ := c.ReceivedSpaces.Load(userID) + rss, _ := c.ReceivedSpaces.Load(userIDKey) receivedSpace := rss.Spaces[spaceID] if receivedSpace.States == nil { receivedSpace.States = map[string]*State{} @@ -128,7 +130,7 @@ func (c *Cache) Add(ctx context.Context, userID, spaceID string, rs *collaborati log := appctx.GetLogger(ctx).With(). Str("hostname", os.Getenv("HOSTNAME")). - Str("userID", userID). + Str("userIDKey", userIDKey). Str("spaceID", spaceID).Logger() var err error @@ -167,18 +169,19 @@ func (c *Cache) Add(ctx context.Context, userID, spaceID string, rs *collaborati } // Get returns one entry from the cache -func (c *Cache) Get(ctx context.Context, userID, spaceID, shareID string) (*State, error) { +func (c *Cache) Get(ctx context.Context, userID utils.FilenameEncoder, spaceID, shareID string) (*State, error) { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") + userIDKey := userID.SafeFilename() unlock := c.lockUser(userID) span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) defer unlock() err := c.syncWithLock(ctx, userID) if err != nil { return nil, err } - rss, ok := c.ReceivedSpaces.Load(userID) + rss, ok := c.ReceivedSpaces.Load(userIDKey) if !ok || rss.Spaces[spaceID] == nil { return nil, nil } @@ -186,21 +189,22 @@ func (c *Cache) Get(ctx context.Context, userID, spaceID, shareID string) (*Stat } // Remove removes an entry from the cache -func (c *Cache) Remove(ctx context.Context, userID, spaceID, shareID string) error { +func (c *Cache) Remove(ctx context.Context, userID utils.FilenameEncoder, spaceID, shareID string) error { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") + userIDKey := userID.SafeFilename() unlock := c.lockUser(userID) span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) defer unlock() ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userID), attribute.String("cs3.spaceid", spaceID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey), attribute.String("cs3.spaceid", spaceID)) persistFunc := func() error { - c.initializeIfNeeded(userID, spaceID) + c.initializeIfNeeded(userIDKey, spaceID) - rss, _ := c.ReceivedSpaces.Load(userID) + rss, _ := c.ReceivedSpaces.Load(userIDKey) receivedSpace := rss.Spaces[spaceID] if receivedSpace.States == nil { receivedSpace.States = map[string]*State{} @@ -215,7 +219,7 @@ func (c *Cache) Remove(ctx context.Context, userID, spaceID, shareID string) err log := appctx.GetLogger(ctx).With(). Str("hostname", os.Getenv("HOSTNAME")). - Str("userID", userID). + Str("userIDKey", userIDKey). Str("spaceID", spaceID).Logger() var err error @@ -255,11 +259,12 @@ func (c *Cache) Remove(ctx context.Context, userID, spaceID, shareID string) err // List returns a list of received shares for a given user // The return list is guaranteed to be thread-safe -func (c *Cache) List(ctx context.Context, userID string) (map[string]*Space, error) { +func (c *Cache) List(ctx context.Context, userID utils.FilenameEncoder) (map[string]*Space, error) { + userIDKey := userID.SafeFilename() ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") unlock := c.lockUser(userID) span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) defer unlock() err := c.syncWithLock(ctx, userID) @@ -268,7 +273,7 @@ func (c *Cache) List(ctx context.Context, userID string) (map[string]*Space, err } spaces := map[string]*Space{} - rss, _ := c.ReceivedSpaces.Load(userID) + rss, _ := c.ReceivedSpaces.Load(userIDKey) for spaceID, space := range rss.Spaces { spaceCopy := &Space{ States: map[string]*State{}, @@ -285,19 +290,20 @@ func (c *Cache) List(ctx context.Context, userID string) (map[string]*Space, err return spaces, nil } -func (c *Cache) syncWithLock(ctx context.Context, userID string) error { +func (c *Cache) syncWithLock(ctx context.Context, userID utils.FilenameEncoder) error { + userIDKey := userID.SafeFilename() ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Sync") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) - log := appctx.GetLogger(ctx).With().Str("userID", userID).Logger() + log := appctx.GetLogger(ctx).With().Str("userIDKey", userIDKey).Logger() - c.initializeIfNeeded(userID, "") + c.initializeIfNeeded(userIDKey, "") - jsonPath := userJSONPath(userID) + jsonPath := userJSONPath(userIDKey) span.AddEvent("updating cache") // - update cached list of created shares for the user in memory if changed - rss, _ := c.ReceivedSpaces.Load(userID) + rss, _ := c.ReceivedSpaces.Load(userIDKey) dlres, err := c.storage.Download(ctx, metadata.DownloadRequest{ Path: jsonPath, IfNoneMatch: []string{rss.etag}, @@ -326,18 +332,19 @@ func (c *Cache) syncWithLock(ctx context.Context, userID string) error { } newSpaces.etag = dlres.Etag - c.ReceivedSpaces.Store(userID, newSpaces) + c.ReceivedSpaces.Store(userIDKey, newSpaces) span.SetStatus(codes.Ok, "") return nil } // persist persists the data for one user to the storage -func (c *Cache) persist(ctx context.Context, userID string) error { +func (c *Cache) persist(ctx context.Context, userID utils.FilenameEncoder) error { + userIDKey := userID.SafeFilename() ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Persist") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userID)) + span.SetAttributes(attribute.String("cs3.userid.key", userIDKey)) - rss, ok := c.ReceivedSpaces.Load(userID) + rss, ok := c.ReceivedSpaces.Load(userIDKey) if !ok { span.SetStatus(codes.Ok, "no received shares") return nil @@ -349,7 +356,7 @@ func (c *Cache) persist(ctx context.Context, userID string) error { span.SetStatus(codes.Error, err.Error()) return err } - jsonPath := userJSONPath(userID) + jsonPath := userJSONPath(userIDKey) if err := c.storage.MakeDirIfNotExist(ctx, path.Dir(jsonPath)); err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache/sharecache.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache/sharecache.go index d7a830594a..c5a812dc16 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache/sharecache.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache/sharecache.go @@ -37,6 +37,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/shareid" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/mtimesyncedcache" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + "github.com/opencloud-eu/reva/v2/pkg/utils" ) // name is the Tracer name used to identify this instrumentation library. @@ -89,15 +90,17 @@ func New(s metadata.Storage, namespace, filename string, ttl time.Duration) Cach } // Add adds a share to the cache -func (c *Cache) Add(ctx context.Context, userid, shareID string) error { +func (c *Cache) Add(ctx context.Context, id utils.FilenameEncoder, shareID string) error { + key := id.SafeFilename() + ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") - unlock := c.lockUser(userid) + unlock := c.lockUser(key) span.End() - span.SetAttributes(attribute.String("cs3.userid", userid)) + span.SetAttributes(attribute.String("cs3.userid", key)) defer unlock() - if _, ok := c.UserShares.Load(userid); !ok { - err := c.syncWithLock(ctx, userid) + if _, ok := c.UserShares.Load(key); !ok { + err := c.syncWithLock(ctx, key) if err != nil { return err } @@ -105,23 +108,23 @@ func (c *Cache) Add(ctx context.Context, userid, shareID string) error { ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userid), attribute.String("cs3.shareid", shareID)) + span.SetAttributes(attribute.String("cs3.userid", key), attribute.String("cs3.shareid", shareID)) storageid, spaceid, _ := shareid.Decode(shareID) ssid := storageid + shareid.IDDelimiter + spaceid persistFunc := func() error { - c.initializeIfNeeded(userid, ssid) + c.initializeIfNeeded(key, ssid) // add share id - us, _ := c.UserShares.Load(userid) + us, _ := c.UserShares.Load(key) us.UserShares[ssid].IDs[shareID] = struct{}{} - return c.Persist(ctx, userid) + return c.Persist(ctx, key) } log := appctx.GetLogger(ctx).With(). Str("hostname", os.Getenv("HOSTNAME")). - Str("userID", userid). + Str("userID", key). Str("shareID", shareID).Logger() var err error @@ -149,7 +152,7 @@ func (c *Cache) Add(ctx context.Context, userid, shareID string) error { log.Error().Err(err).Msg("persisting added share failed") return err } - if err := c.syncWithLock(ctx, userid); err != nil { + if err := c.syncWithLock(ctx, key); err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) log.Error().Err(err).Msg("persisting added share failed. giving up.") @@ -160,15 +163,16 @@ func (c *Cache) Add(ctx context.Context, userid, shareID string) error { } // Remove removes a share for the given user -func (c *Cache) Remove(ctx context.Context, userid, shareID string) error { +func (c *Cache) Remove(ctx context.Context, id utils.FilenameEncoder, shareID string) error { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") - unlock := c.lockUser(userid) + key := id.SafeFilename() + unlock := c.lockUser(key) span.End() - span.SetAttributes(attribute.String("cs3.userid", userid)) + span.SetAttributes(attribute.String("cs3.userid", key)) defer unlock() - if _, ok := c.UserShares.Load(userid); ok { - err := c.syncWithLock(ctx, userid) + if _, ok := c.UserShares.Load(key); ok { + err := c.syncWithLock(ctx, key) if err != nil { return err } @@ -176,13 +180,13 @@ func (c *Cache) Remove(ctx context.Context, userid, shareID string) error { ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Remove") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userid), attribute.String("cs3.shareid", shareID)) + span.SetAttributes(attribute.String("cs3.userid", key), attribute.String("cs3.shareid", shareID)) storageid, spaceid, _ := shareid.Decode(shareID) ssid := storageid + shareid.IDDelimiter + spaceid persistFunc := func() error { - us, loaded := c.UserShares.LoadOrStore(userid, &UserShareCache{ + us, loaded := c.UserShares.LoadOrStore(key, &UserShareCache{ UserShares: map[string]*SpaceShareIDs{}, }) @@ -191,12 +195,12 @@ func (c *Cache) Remove(ctx context.Context, userid, shareID string) error { delete(us.UserShares[ssid].IDs, shareID) } - return c.Persist(ctx, userid) + return c.Persist(ctx, key) } log := appctx.GetLogger(ctx).With(). Str("hostname", os.Getenv("HOSTNAME")). - Str("userID", userid). + Str("userID", key). Str("shareID", shareID).Logger() var err error @@ -224,7 +228,7 @@ func (c *Cache) Remove(ctx context.Context, userid, shareID string) error { log.Error().Err(err).Msg("persisting removed share failed") return err } - if err := c.syncWithLock(ctx, userid); err != nil { + if err := c.syncWithLock(ctx, key); err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return err @@ -235,18 +239,19 @@ func (c *Cache) Remove(ctx context.Context, userid, shareID string) error { } // List return the list of spaces/shares for the given user/group -func (c *Cache) List(ctx context.Context, userid string) (map[string]SpaceShareIDs, error) { +func (c *Cache) List(ctx context.Context, id utils.FilenameEncoder) (map[string]SpaceShareIDs, error) { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") - unlock := c.lockUser(userid) + key := id.SafeFilename() + unlock := c.lockUser(key) span.End() - span.SetAttributes(attribute.String("cs3.userid", userid)) + span.SetAttributes(attribute.String("cs3.userid", key)) defer unlock() - if err := c.syncWithLock(ctx, userid); err != nil { + if err := c.syncWithLock(ctx, key); err != nil { return nil, err } r := map[string]SpaceShareIDs{} - us, ok := c.UserShares.Load(userid) + us, ok := c.UserShares.Load(key) if !ok { return r, nil } @@ -309,12 +314,12 @@ func (c *Cache) syncWithLock(ctx context.Context, userID string) error { } // Persist persists the data for one user/group to the storage -func (c *Cache) Persist(ctx context.Context, userid string) error { +func (c *Cache) Persist(ctx context.Context, key string) error { ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Persist") defer span.End() - span.SetAttributes(attribute.String("cs3.userid", userid)) + span.SetAttributes(attribute.String("cs3.userid", key)) - us, ok := c.UserShares.Load(userid) + us, ok := c.UserShares.Load(key) if !ok { span.SetStatus(codes.Ok, "no user shares") return nil @@ -325,7 +330,7 @@ func (c *Cache) Persist(ctx context.Context, userid string) error { span.SetStatus(codes.Error, err.Error()) return err } - jsonPath := c.userCreatedPath(userid) + jsonPath := c.userCreatedPath(key) if err := c.storage.MakeDirIfNotExist(ctx, path.Dir(jsonPath)); err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -355,8 +360,8 @@ func (c *Cache) Persist(ctx context.Context, userid string) error { return nil } -func (c *Cache) userCreatedPath(userid string) string { - return filepath.Join("/", c.namespace, userid, c.filename) +func (c *Cache) userCreatedPath(key string) string { + return filepath.Join("/", c.namespace, key, c.filename) } func (c *Cache) initializeIfNeeded(userid, ssid string) { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/decomposedfs.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/decomposedfs.go index ccd7f05d34..9ae5f28155 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/decomposedfs.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/decomposedfs.go @@ -130,6 +130,7 @@ type Decomposedfs struct { UserCache *ttlcache.Cache userSpaceIndex *spaceidindex.Index groupSpaceIndex *spaceidindex.Index + mailSpaceIndex *spaceidindex.Index spaceTypeIndex *spaceidindex.Index log *zerolog.Logger @@ -218,6 +219,11 @@ func New(o *options.Options, aspects aspects.Aspects, log *zerolog.Logger) (stor if err != nil { return nil, err } + mailSpaceIndex := spaceidindex.New(filepath.Join(o.Root, lookup.IndexesDir), "by-mail") + err = mailSpaceIndex.Init() + if err != nil { + return nil, err + } spaceTypeIndex := spaceidindex.New(filepath.Join(o.Root, lookup.IndexesDir), "by-type") err = spaceTypeIndex.Init() if err != nil { @@ -244,6 +250,7 @@ func New(o *options.Options, aspects aspects.Aspects, log *zerolog.Logger) (stor UserCache: ttlcache.NewCache(), userSpaceIndex: userSpaceIndex, groupSpaceIndex: groupSpaceIndex, + mailSpaceIndex: mailSpaceIndex, spaceTypeIndex: spaceTypeIndex, log: log, } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/grants.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/grants.go index 94cc53c355..4a1eb82ed8 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/grants.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/grants.go @@ -23,6 +23,7 @@ import ( "path/filepath" "strings" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/opencloud-eu/reva/v2/internal/grpc/services/storageprovider" "github.com/opencloud-eu/reva/v2/pkg/appctx" @@ -234,9 +235,15 @@ func (fs *Decomposedfs) RemoveGrant(ctx context.Context, ref *provider.Reference // invalidate space grant switch g.Grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: - // remove from user index - if err := fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), grantNode.SpaceID); err != nil { - return err + filename := utils.NewFSSafeUserID(g.Grantee.GetUserId()).SafeFilename() + if g.Grantee.GetUserId().GetType() == userpb.UserType_USER_TYPE_GUEST { + if err := fs.mailSpaceIndex.Remove(filename, grantNode.SpaceID); err != nil { + return err + } + } else { + if err := fs.userSpaceIndex.Remove(filename, grantNode.SpaceID); err != nil { + return err + } } case provider.GranteeType_GRANTEE_TYPE_GROUP: // remove from group index @@ -320,7 +327,7 @@ func (fs *Decomposedfs) loadGrant(ctx context.Context, ref *provider.Reference, for _, grant := range grants { switch grant.Grantee.GetType() { case provider.GranteeType_GRANTEE_TYPE_USER: - if g.Grantee.GetUserId().GetOpaqueId() == grant.Grantee.GetUserId().GetOpaqueId() { + if utils.UserIDEqual(g.Grantee.GetUserId(), grant.Grantee.GetUserId()) { return n, unlockFunc, grant, nil } case provider.GranteeType_GRANTEE_TYPE_GROUP: diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/prefixes.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/prefixes.go index 0c36497373..45851fa794 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/prefixes.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/prefixes.go @@ -20,6 +20,7 @@ package prefixes import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace" ) // Declare a list of xattr keys @@ -53,8 +54,9 @@ const ( // grantPrefix is the prefix for sharing related extended attributes GrantPrefix string = OcPrefix + "grant." - GrantUserAcePrefix string = OcPrefix + "grant." + UserAcePrefix - GrantGroupAcePrefix string = OcPrefix + "grant." + GroupAcePrefix + GrantUserAcePrefix string = OcPrefix + "grant." + ace.UserAcePrefix + GrantGroupAcePrefix string = OcPrefix + "grant." + ace.GroupAcePrefix + GrantMailAcePrefix string = OcPrefix + "grant." + ace.MailAcePrefix MetadataPrefix string = OcPrefix + "md." // favorite flag, per user @@ -103,9 +105,6 @@ const ( SpaceAliasAttr string = OcPrefix + "space.alias" SpaceTenantIDAttr string = OcPrefix + "space.tenantid" SpaceContentTypeAttr string = OcPrefix + "space.contenttype" - - UserAcePrefix string = "u:" - GroupAcePrefix string = "g:" ) func FavoriteKey(uid *userpb.UserId) string { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node/node.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node/node.go index dc03ad6b85..33516a6a64 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node/node.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node/node.go @@ -1238,7 +1238,7 @@ func (n *Node) IsDenied(ctx context.Context) bool { isExecutant := func(g *provider.Grantee) bool { switch g.GetType() { case provider.GranteeType_GRANTEE_TYPE_USER: - return g.GetUserId().GetOpaqueId() == u.GetId().GetOpaqueId() + return utils.UserIDEqual(g.GetUserId(), u.GetId()) case provider.GranteeType_GRANTEE_TYPE_GROUP: // check gid gid := g.GetGroupId().GetOpaqueId() @@ -1300,14 +1300,19 @@ func (n *Node) ReadGrant(ctx context.Context, grantee string) (g *provider.Grant return e.Grant(), nil } -// ReadGrant reads a CS3 grant +// DeleteGrant reads a CS3 grant func (n *Node) DeleteGrant(ctx context.Context, g *provider.Grant) (err error) { var attr string - if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + switch g.Grantee.Type { + case provider.GranteeType_GRANTEE_TYPE_GROUP: attr = prefixes.GrantGroupAcePrefix + g.Grantee.GetGroupId().OpaqueId - } else { - attr = prefixes.GrantUserAcePrefix + g.Grantee.GetUserId().OpaqueId + case provider.GranteeType_GRANTEE_TYPE_USER: + attr = prefixes.GrantPrefix + ace.UserAce(g.Grantee.GetUserId()) + default: + // Note: We shouldn't actually get here as this is already caught in the upper + // layers (e.g. the storage provider) + return errors.New("grantee type is invalid") } if err = n.RemoveXattr(ctx, attr); err != nil { @@ -1353,7 +1358,7 @@ func (n *Node) getGranteeTypes(ctx context.Context) []provider.GranteeType { hasUserShares, hasGroupShares := false, false for i := range g { switch { - case !hasUserShares && strings.HasPrefix(g[i], prefixes.GrantUserAcePrefix): + case !hasUserShares && (strings.HasPrefix(g[i], prefixes.GrantUserAcePrefix) || strings.HasPrefix(g[i], prefixes.GrantMailAcePrefix)): hasUserShares = true case !hasGroupShares && strings.HasPrefix(g[i], prefixes.GrantGroupAcePrefix): hasGroupShares = true diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/spaces.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/spaces.go index 00dd5e5246..2421c3c074 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/spaces.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/spaces.go @@ -345,7 +345,8 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide var err error if requestedUserID != nil { - allMatches, err = fs.userSpaceIndex.Load(requestedUserID.GetOpaqueId()) + filename := utils.NewFSSafeUserID(requestedUserID).SafeFilename() + allMatches, err = fs.userSpaceIndex.Load(filename) // do not return an error if the user has no spaces if err != nil && !os.IsNotExist(err) { return nil, errors.Wrap(err, "error reading user index") @@ -357,6 +358,21 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide matches[allMatches[entry]] = allMatches[entry] } + // guests are granted shares via their mail address, search the mail index as well + if requestedUserID.GetType() == userv1beta1.UserType_USER_TYPE_GUEST { + allMatches, err = fs.mailSpaceIndex.Load(filename) + // do not return an error if the guest has no spaces + if err != nil && !os.IsNotExist(err) { + return nil, errors.Wrap(err, "error reading mail index") + } + + if entry == spaceIDAny { + maps.Copy(matches, allMatches) + } else { + matches[allMatches[entry]] = allMatches[entry] + } + } + // get Groups for userid user := ctxpkg.ContextMustGetUser(ctx) // TODO the user from context may not have groups populated @@ -788,9 +804,17 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De for _, g := range grants { switch g.Grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: - // remove from user index - if err := fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), spaceID); err != nil { - sublog.Error().Err(err).Str("grantee", g.Grantee.GetUserId().GetOpaqueId()).Msg("could not remove user from user index") + filename := utils.NewFSSafeUserID(g.Grantee.GetUserId()).SafeFilename() + if g.Grantee.GetUserId().GetType() == userv1beta1.UserType_USER_TYPE_GUEST { + // remove from mail index + if err := fs.mailSpaceIndex.Remove(filename, spaceID); err != nil { + sublog.Error().Err(err).Str("grantee", g.Grantee.GetUserId().GetOpaqueId()).Msg("could not remove guest from mail index") + } + } else { + // remove from user index + if err := fs.userSpaceIndex.Remove(filename, spaceID); err != nil { + sublog.Error().Err(err).Str("grantee", g.Grantee.GetUserId().GetOpaqueId()).Msg("could not remove user from user index") + } } case provider.GranteeType_GRANTEE_TYPE_GROUP: // remove from group index @@ -894,7 +918,11 @@ func (fs *Decomposedfs) updateIndexes(ctx context.Context, grantee *provider.Gra // create space grant index switch grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: - return fs.linkSpaceByUser(ctx, grantee.GetUserId().GetOpaqueId(), spaceID, target) + filename := utils.NewFSSafeUserID(grantee.GetUserId()).SafeFilename() + if grantee.GetUserId().GetType() == userv1beta1.UserType_USER_TYPE_GUEST { + return fs.linkSpaceByMail(ctx, filename, spaceID, target) + } + return fs.linkSpaceByUser(ctx, filename, spaceID, target) case provider.GranteeType_GRANTEE_TYPE_GROUP: return fs.linkSpaceByGroup(ctx, grantee.GetGroupId().GetOpaqueId(), spaceID, target) default: @@ -910,6 +938,10 @@ func (fs *Decomposedfs) linkSpaceByGroup(ctx context.Context, groupID, spaceID, return fs.groupSpaceIndex.Add(groupID, spaceID, target) } +func (fs *Decomposedfs) linkSpaceByMail(ctx context.Context, mail, spaceID, target string) error { + return fs.mailSpaceIndex.Add(mail, spaceID, target) +} + func (fs *Decomposedfs) linkStorageSpaceType(ctx context.Context, spaceType, spaceID, target string) error { return fs.spaceTypeIndex.Add(spaceType, spaceID, target) } @@ -983,10 +1015,19 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, // invalidate space grant switch g.Grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: - // remove from user index - if err := fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), n.GetSpaceID()); err != nil { - sublog.Error().Err(err).Str("grantee", id). - Msg("failed to delete expired user space index") + filename := utils.NewFSSafeUserID(g.Grantee.GetUserId()).SafeFilename() + if g.Grantee.GetUserId().GetType() == userv1beta1.UserType_USER_TYPE_GUEST { + // remove from mail index + if err := fs.mailSpaceIndex.Remove(filename, n.GetSpaceID()); err != nil { + sublog.Error().Err(err).Str("grantee", id). + Msg("failed to delete expired mail space index") + } + } else { + // remove from user index + if err := fs.userSpaceIndex.Remove(filename, n.GetSpaceID()); err != nil { + sublog.Error().Err(err).Str("grantee", id). + Msg("failed to delete expired user space index") + } } case provider.GranteeType_GRANTEE_TYPE_GROUP: // remove from group index diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace/ace.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace/ace.go index ef3da2ee29..96f4fa180e 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace/ace.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace/ace.go @@ -31,6 +31,13 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants" + "github.com/opencloud-eu/reva/v2/pkg/utils" +) + +const ( + UserAcePrefix = "u:" + GroupAcePrefix = "g:" + MailAcePrefix = "m:" ) /* @@ -202,7 +209,7 @@ func FromGrant(g *provider.Grant) *ACE { } if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { e.flags = "g" - e.principal = "g:" + g.Grantee.GetGroupId().OpaqueId + e.principal = GroupAcePrefix + g.Grantee.GetGroupId().OpaqueId } else { e.principal = UserAce(g.Grantee.GetUserId()) } @@ -215,7 +222,13 @@ func FromGrant(g *provider.Grant) *ACE { } func UserAce(id *userpb.UserId) string { - return "u:" + id.OpaqueId + filename := utils.NewFSSafeUserID(id).SafeFilename() + switch id.GetType() { + case userpb.UserType_USER_TYPE_GUEST: + return MailAcePrefix + filename + default: + return UserAcePrefix + filename + } } // Principal returns the principal of the ACE, eg. `u:` or `g:` @@ -256,13 +269,19 @@ func Unmarshal(principal string, v []byte) (e *ACE, err error) { } // check consistency of Flags and principal type if strings.Contains(e.flags, "g") { - if principal[:1] != "g" { + if !strings.HasPrefix(principal, GroupAcePrefix) { return nil, fmt.Errorf("inconsistent ace: expected group") } } else { - if principal[:1] != "u" { + if !strings.HasPrefix(principal, UserAcePrefix) && !strings.HasPrefix(principal, MailAcePrefix) { return nil, fmt.Errorf("inconsistent ace: expected user") } + if strings.HasPrefix(principal, MailAcePrefix) { + id := &userpb.UserId{Type: userpb.UserType_USER_TYPE_GUEST} + if _, err := utils.NewFSSafeUserID(id).Decode(strings.TrimPrefix(principal, MailAcePrefix)); err != nil { + return nil, fmt.Errorf("invalid guest ace principal: %w", err) + } + } } default: return nil, fmt.Errorf("unknown ace encoding") @@ -288,7 +307,14 @@ func (e *ACE) Grant() *provider.Grant { if e.granteeType() == provider.GranteeType_GRANTEE_TYPE_GROUP { g.Grantee.Id = &provider.Grantee_GroupId{GroupId: &grouppb.GroupId{OpaqueId: id}} } else if e.granteeType() == provider.GranteeType_GRANTEE_TYPE_USER { - g.Grantee.Id = &provider.Grantee_UserId{UserId: &userpb.UserId{OpaqueId: id}} + if strings.HasPrefix(e.principal, MailAcePrefix) { + // Guest principals are validated by Unmarshal or encoded by FromGrant, + // so decoding cannot fail for a valid ACE. + userID, _ := utils.NewFSSafeUserID(&userpb.UserId{Type: userpb.UserType_USER_TYPE_GUEST}).Decode(id) + g.Grantee.Id = &provider.Grantee_UserId{UserId: userID} + } else { + g.Grantee.Id = &provider.Grantee_UserId{UserId: &userpb.UserId{OpaqueId: id, Type: userpb.UserType_USER_TYPE_PRIMARY}} + } } if e.expires != 0 { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes/prefixes.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes/prefixes.go index fb0cbef28b..855b3998ef 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes/prefixes.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes/prefixes.go @@ -18,6 +18,8 @@ package prefixes +import "github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace" + // Declare a list of xattr keys // TODO the below comment is currently copied from the owncloud driver, revisit // Currently,extended file attributes have four separated @@ -49,8 +51,8 @@ const ( // grantPrefix is the prefix for sharing related extended attributes GrantPrefix string = OcisPrefix + "grant." - GrantUserAcePrefix string = OcisPrefix + "grant." + UserAcePrefix - GrantGroupAcePrefix string = OcisPrefix + "grant." + GroupAcePrefix + GrantUserAcePrefix string = OcisPrefix + "grant." + ace.UserAcePrefix + GrantGroupAcePrefix string = OcisPrefix + "grant." + ace.GroupAcePrefix MetadataPrefix string = OcisPrefix + "md." // favorite flag, per user @@ -97,7 +99,4 @@ const ( SpaceReadmeAttr string = OcisPrefix + "space.readme" SpaceImageAttr string = OcisPrefix + "space.image" SpaceAliasAttr string = OcisPrefix + "space.alias" - - UserAcePrefix string = "u:" - GroupAcePrefix string = "g:" ) diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/filename_encoder.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/filename_encoder.go new file mode 100644 index 0000000000..13fdf39257 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/filename_encoder.go @@ -0,0 +1,62 @@ +package utils + +import ( + "encoding/base64" + "strings" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "google.golang.org/protobuf/proto" +) + +type FilenameEncoder interface { + SafeFilename() string +} + +type FSSafeUserID struct { + id *userpb.UserId +} + +func NewFSSafeUserID(id *userpb.UserId) FSSafeUserID { + return FSSafeUserID{id: id} +} + +func (id FSSafeUserID) SafeFilename() string { + opaqueID := id.id.GetOpaqueId() + if id.id.GetType() == userpb.UserType_USER_TYPE_GUEST { + // Guest opaqueID is an email address, which is why we choose to make + // it lowercase: RFC 5321 does specify that email address local-parts + // are case sensitive but, in practice, it's a de-facto standard that + // email providers consider them to be case insensitive: + return base64.RawURLEncoding.EncodeToString([]byte(strings.ToLower(opaqueID))) + } + return opaqueID +} + +// Decode returns a copy of the wrapped user ID with the filename restored as its opaque ID. +// The decoding decision is done based on the Type attribute of the Receiver id +func (id FSSafeUserID) Decode(filename string) (*userpb.UserId, error) { + opaqueID := filename + if id.id.GetType() == userpb.UserType_USER_TYPE_GUEST { + decoded, err := base64.RawURLEncoding.DecodeString(filename) + if err != nil { + return nil, err + } + opaqueID = string(decoded) + } + + decodedID := &userpb.UserId{} + if id.id != nil { + decodedID = proto.Clone(id.id).(*userpb.UserId) + } + decodedID.OpaqueId = opaqueID + return decodedID, nil +} + +type FSSafeGroupID struct { + ID *grouppb.GroupId +} + +func (id FSSafeGroupID) SafeFilename() string { + return id.ID.GetOpaqueId() +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/utils.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/utils.go index bc4833dd27..78a3c960b1 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/utils.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/utils.go @@ -218,12 +218,31 @@ func ExtractGranteeID(grantee *provider.Grantee) (*userpb.UserId, *grouppb.Group // UserEqual returns whether two users have the same field values. func UserEqual(u, v *userpb.UserId) bool { - return u != nil && v != nil && u.Idp == v.Idp && u.OpaqueId == v.OpaqueId + return u != nil && v != nil && u.Idp == v.Idp && UserIDEqual(u, v) } // UserIDEqual returns whether two users have the same opaqueid values. The idp is ignored func UserIDEqual(u, v *userpb.UserId) bool { - return u != nil && v != nil && u.OpaqueId == v.OpaqueId + if u == nil || v == nil { + return false + } + return CanonicalUserID(u) == CanonicalUserID(v) +} + +// CanonicalUserID returns the stable representation of a UserId as e.g. used for user storage keys. +// Currently this is only relevant for UserId of the USER_TYPE_GUEST, which are matched case-insensitively. +func CanonicalUserID(id *userpb.UserId) string { + if id == nil { + return "" + } + if id.GetType() == userpb.UserType_USER_TYPE_GUEST { + // Guest user IDs are lowercased to enable case insensitive + // comparisons: those are email addresses, and while RFC 5321 states + // that the local-part is case sensitive, in practice, it's a de facto + // standard that email providers consider them to be case insensitive. + return strings.ToLower(id.GetOpaqueId()) + } + return id.GetOpaqueId() } // GroupEqual returns whether two groups have the same field values. diff --git a/vendor/modules.txt b/vendor/modules.txt index 4da1ac8c1b..d95c97cf65 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1364,10 +1364,10 @@ 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.20260902170011-45af3945a067 +# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260908131234-71ed393d5010 ## explicit; go 1.23 github.com/opencloud-eu/libre-graph-api-go -# github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774 +# github.com/opencloud-eu/reva/v2 v2.50.0 ## explicit; go 1.25.8 github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace github.com/opencloud-eu/reva/v2/cmd/revad/runtime @@ -1504,7 +1504,6 @@ github.com/opencloud-eu/reva/v2/pkg/eosclient/eosgrpc github.com/opencloud-eu/reva/v2/pkg/eosclient/eosgrpc/eos_grpc github.com/opencloud-eu/reva/v2/pkg/errtypes github.com/opencloud-eu/reva/v2/pkg/events -github.com/opencloud-eu/reva/v2/pkg/events/mocks github.com/opencloud-eu/reva/v2/pkg/events/raw github.com/opencloud-eu/reva/v2/pkg/events/raw/mocks github.com/opencloud-eu/reva/v2/pkg/events/stream