From 08edebc66cda02b8798a4a6b9e14c418833ec305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 7 Oct 2026 12:45:13 +0200 Subject: [PATCH 1/2] Use the stored guest id when returning an invite permission The share provider now stores the guest's mail address in its canonical form, so build the returned identity from the created share instead of the request. --- .../service/v0/api_driveitem_permissions.go | 3 +- .../v0/api_driveitem_permissions_test.go | 29 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index e19eec5aa8..7d43726744 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -182,7 +182,8 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto cTime = createShareResponse.GetShare().GetCtime() expiration = createShareResponse.GetShare().GetExpiration() - guestIdentity, err := cs3UserIdToIdentity(ctx, s.identityCache, createShareRequest.GetGrant().GetGrantee().GetUserId()) + // the share provider stores the guest's mail address in its canonical form + guestIdentity, err := cs3UserIdToIdentity(ctx, s.identityCache, createShareResponse.GetShare().GetGrantee().GetUserId()) if err != nil { s.logger.Debug().Err(err).Msg("failed to convert guest user id to identity") return libregraph.Permission{}, err 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 5c386b69f6..cf6ecd44c7 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -176,7 +176,8 @@ var _ = Describe("DriveItemPermissionsService", func() { {Email: libregraph.PtrString("guest@example.com")}, } createShareResponse.Share = &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: "guest123"}, + Id: &collaboration.ShareId{OpaqueId: "guest123"}, + Grantee: guestGrantee("guest@example.com"), } permission, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) @@ -187,6 +188,23 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(permission.GrantedToV2.User.GetLibreGraphUserType()).To(Equal("Guest")) }) + It("returns the guest id as stored by the share provider", func() { + cfg.Commons = &shared.Commons{EnableGuestLinks: true} + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {Email: libregraph.PtrString("Guest@\u0130nfocorp.com")}, + } + createShareResponse.Share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "guest123"}, + Grantee: guestGrantee("guest@xn--infocorp-o0e.com"), + } + + permission, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + Expect(permission.GrantedToV2.User.GetId()).To(Equal("guest@xn--infocorp-o0e.com")) + }) + It("rejects guest shares when guest invites are disabled by default", func() { driveItemInvite.Recipients = []libregraph.DriveRecipient{ {Email: libregraph.PtrString("guest@example.com")}, @@ -1477,3 +1495,12 @@ var _ = Describe("DriveItemPermissionsApi", func() { }) }) }) + +func guestGrantee(mail string) *provider.Grantee { + return &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{Type: userpb.UserType_USER_TYPE_GUEST, OpaqueId: mail}, + }, + } +} From 50a004b04fb401b282d17778681d9357d9a7a977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 7 Oct 2026 16:22:23 +0200 Subject: [PATCH 2/2] Bump reva --- go.mod | 2 +- go.sum | 4 +- .../usershareprovider/usershareprovider.go | 11 +-- .../pkg/auth/manager/guestlinks/guestlinks.go | 4 + .../reva/v2/pkg/events/postprocessing.go | 21 +++-- .../reva/v2/pkg/storage/fs/posix/posix.go | 4 + .../pkg/storage/fs/posix/tree/assimilation.go | 24 +++--- .../storage/pkg/decomposedfs/decomposedfs.go | 10 +++ .../decomposedfs/metadata/hybrid_backend.go | 12 +-- .../metadata/prefixes/oc_prefix.go | 2 +- .../metadata/prefixes/oc_prefix_freebsd.go | 2 +- .../metadata/prefixes/prefixes.go | 78 ++++++++++++++++++- .../pkg/decomposedfs/options/options.go | 3 + .../utils/decomposedfs/decomposedfs.go | 5 ++ .../reva/v2/pkg/utils/filename_encoder.go | 3 +- .../opencloud-eu/reva/v2/pkg/utils/mail.go | 37 +++++++++ .../opencloud-eu/reva/v2/pkg/utils/utils.go | 26 ++++++- vendor/modules.txt | 2 +- 18 files changed, 212 insertions(+), 38 deletions(-) create mode 100644 vendor/github.com/opencloud-eu/reva/v2/pkg/utils/mail.go diff --git a/go.mod b/go.mod index 7062f68617..c5a5b9b644 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/open-policy-agent/opa v1.21.1 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 github.com/opencloud-eu/libre-graph-api-go v1.0.8 - github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424 + github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45 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 diff --git a/go.sum b/go.sum index 3ed91fa9b5..a0fb3186f1 100644 --- a/go.sum +++ b/go.sum @@ -761,8 +761,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l 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 h1:VTkFLkLNna+T4RN6JUZnVQ1bqVAfPtMeORXbFzEYGKs= github.com/opencloud-eu/libre-graph-api-go v1.0.8/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= -github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424 h1:rvopdGQTsI0PT7P3MRamrDsO1yYIKJBOxJAbbYmc4P8= -github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424/go.mod h1:ngLsDakKnt8zCFCeULhuSZFESGy5NIuSq5/JGeOhPdU= +github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45 h1:rrZBLsznQDL6YFjtbZF+Z8WCHbGSWn0RsD0ddwaVY8c= +github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45/go.mod h1:rAwuI3PX8n3RxBXHfpMtAKqfwxTIKhZol1Bi3Qj39p8= 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/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 1b769774bc..560cf9df31 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,7 +20,6 @@ package usershareprovider import ( "context" - "net/mail" "path/filepath" "regexp" "slices" @@ -184,13 +183,15 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar 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 != "" { + // and store them in their canonical form + guestID := req.GetGrant().GetGrantee().GetUserId() + canonical, err := utils.CanonicalMail(guestID.GetOpaqueId()) + if err != nil { return &collaboration.CreateShareResponse{ - Status: status.NewInvalidArg(ctx, "invalid mail address for guest grantee"), + Status: status.NewInvalidArg(ctx, "invalid mail address for guest grantee: "+err.Error()), }, nil } + guestID.OpaqueId = canonical } } gatewayClient, err := s.gatewaySelector.Next() diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go index a8c63a233d..837ec63504 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/auth/manager/guestlinks/guestlinks.go @@ -271,6 +271,10 @@ func guestUserFromShare(s *collaboration.Share) (*userpb.User, error) { return nil, errors.New("guestlinks: grantee is not a guest user") } + if !utils.IsASCII(uid.GetOpaqueId()) { + return nil, errors.New("guestlinks: grantee opaque id is not ASCII") + } + addr, err := mail.ParseAddress(uid.GetOpaqueId()) if err != nil || addr.Name != "" || addr.Address != uid.GetOpaqueId() { return nil, errors.New("guestlinks: grantee opaque id is not a bare email address") diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/postprocessing.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/postprocessing.go index b2694dd524..e443f82947 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/events/postprocessing.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/events/postprocessing.go @@ -183,12 +183,21 @@ type UploadReady struct { SpaceOwner *user.UserId ExecutingUser *user.User ImpersonatingUser *user.User - FileRef *provider.Reference - ParentID *provider.ResourceId - Timestamp *types.Timestamp - Failed bool - IsVersion bool - // add reference here? We could use it to inform client pp is finished + // FileRef is a legacy reference to the uploaded file and its shape depends on + // the producer: decomposedfs anchors it at the space root + // (ResourceId.OpaqueId == SpaceId, with a Path relative to that anchor), while + // the posix assimilation emits the file node ID with an empty Path. Neither + // OpaqueId nor Path can be relied on universally, so treat Path as optional + // and use ResourceID to identify the file. + FileRef *provider.Reference + ParentID *provider.ResourceId + // ResourceID identifies the uploaded file: OpaqueId is the node ID. It mirrors + // BytesReceived and is the only file reference every producer sets the same + // way, so id based consumers (Graph API, fileid lookups) should use it. + ResourceID *provider.ResourceId + Timestamp *types.Timestamp + Failed bool + IsVersion bool } // Unmarshal to fulfill umarshaller interface diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/posix.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/posix.go index 43a5f488ac..3588b93719 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/posix.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/posix.go @@ -46,6 +46,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/aspects" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata" + "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/permissions" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload" @@ -105,6 +106,9 @@ func New(o *options.Options, stream events.Stream, cache, historyCache *idcache. if o.FileMetadataCache.Store == "noop" { return nil, fmt.Errorf("the posix driver requires a file metadata cache") } + if err := prefixes.SetOcPrefix(o.MetadataPrefix); err != nil { + return nil, err + } var err error if log == nil { diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go index 53d3d2e6f8..2815dc07b2 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree/assimilation.go @@ -67,7 +67,7 @@ type queueItem struct { timer *time.Timer } -const dirtyFlag = prefixes.OcPrefix + "dirty" +func dirtyFlag() string { return prefixes.OcPrefix + "dirty" } type assimilationNode struct { path string @@ -601,10 +601,11 @@ func (t *Tree) assimilate(item scanItem) error { }) } else { t.PublishEvent(events.UploadReady{ - FileRef: ref, - ParentID: parentResourceID, - Timestamp: utils.TSNow(), - IsVersion: true, + FileRef: ref, + ParentID: parentResourceID, + ResourceID: ref.ResourceId, + Timestamp: utils.TSNow(), + IsVersion: true, }) } } @@ -671,10 +672,11 @@ func (t *Tree) assimilate(item scanItem) error { }) } else { t.PublishEvent(events.UploadReady{ - FileRef: ref, - ParentID: parentId, - Timestamp: utils.TSNow(), - IsVersion: false, + FileRef: ref, + ParentID: parentId, + ResourceID: ref.ResourceId, + Timestamp: utils.TSNow(), + IsVersion: false, }) } } @@ -1088,11 +1090,11 @@ func (t *Tree) propagateSizeDiff(n *node.Node, size int64) error { } func (t *Tree) setDirty(path string, dirty bool) error { - return xattr.Set(path, dirtyFlag, []byte(fmt.Sprintf("%t", dirty))) + return xattr.Set(path, dirtyFlag(), []byte(fmt.Sprintf("%t", dirty))) } func (t *Tree) isDirty(path string) (bool, error) { - dirtyAttr, err := xattr.Get(path, dirtyFlag) + dirtyAttr, err := xattr.Get(path, dirtyFlag()) if err != nil { if metadata.IsAttrUnset(err) { return true, nil 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 78c8afa7b9..367708c520 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 @@ -51,6 +51,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/aspects" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/lookup" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata" + "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options" "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/permissions" @@ -147,6 +148,10 @@ func NewDefault(m map[string]interface{}, bs node.Blobstore, es events.Stream, l return nil, err } + if err := prefixes.SetOcPrefix(o.MetadataPrefix); err != nil { + return nil, err + } + var lu *lookup.Lookup switch o.MetadataBackend { case "xattrs": @@ -415,6 +420,11 @@ func (fs *Decomposedfs) handlePostprocessingEvent(ctx context.Context, event eve }, Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())), }, + ResourceID: &provider.ResourceId{ + StorageId: session.ProviderID(), + SpaceId: session.SpaceID(), + OpaqueId: session.NodeID(), + }, ParentID: parentId, Timestamp: utils.TimeToTS(now), SpaceOwner: n.SpaceOwnerOrManager(ctx), diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/hybrid_backend.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/hybrid_backend.go index cc8989d2d8..be006150b3 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/hybrid_backend.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/hybrid_backend.go @@ -21,7 +21,7 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes" ) -var _metadataOffloadedAttr = prefixes.OcPrefix + "metadata_offloaded" +func metadataOffloadedAttr() string { return prefixes.OcPrefix + "metadata_offloaded" } type MetadataPathFunc func(MetadataNode) string @@ -72,7 +72,7 @@ func (b HybridBackend) Get(ctx context.Context, n MetadataNode, key string) ([]b if isOffloadingAttribute(key) { // check if key is offloaded - offloaded, err := xattr.Get(n.InternalPath(), _metadataOffloadedAttr) + offloaded, err := xattr.Get(n.InternalPath(), metadataOffloadedAttr()) if err == nil && string(offloaded) == "1" { msgpackAttribs := map[string][]byte{} msgBytes, err := os.ReadFile(b.MetadataPath(n)) @@ -174,7 +174,7 @@ func (b HybridBackend) getAll(ctx context.Context, n MetadataNode, skipOffloaded } // merge the attributes from the offload file - offloaded, err := xattr.Get(path, _metadataOffloadedAttr) + offloaded, err := xattr.Get(path, metadataOffloadedAttr()) if err != nil && !IsAttrUnset(err) { return nil, err } @@ -217,7 +217,7 @@ func (b HybridBackend) SetMultiple(ctx context.Context, n MetadataNode, attribs defer func() { _ = unlock() }() } - offloadAttr, err := xattr.Get(path, _metadataOffloadedAttr) + offloadAttr, err := xattr.Get(path, metadataOffloadedAttr()) offloaded := err == nil && string(offloadAttr) == "1" // offload if the offloading metadata size exceeds the limit @@ -346,7 +346,7 @@ func (b HybridBackend) offloadMetadata(ctx context.Context, n MetadataNode) erro } // set the metadata offloaded attribute - err = xattr.Set(path, _metadataOffloadedAttr, []byte("1")) + err = xattr.Set(path, metadataOffloadedAttr(), []byte("1")) if err != nil { return err } @@ -376,7 +376,7 @@ func (b HybridBackend) Remove(ctx context.Context, n MetadataNode, key string) e } if isOffloadingAttribute(key) { - offloadAttr, err := xattr.Get(path, _metadataOffloadedAttr) + offloadAttr, err := xattr.Get(path, metadataOffloadedAttr()) offloaded := err == nil && string(offloadAttr) == "1" if offloaded { // remove from offloaded metadata diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix.go index d6133594a1..7af5cc4e8f 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix.go @@ -24,6 +24,6 @@ package prefixes // manipulate the user. namespace, which is what is used to store decomposedfs // specific metadata. To prevent name collisions with other apps, we are going // to introduce a sub namespace "user.oc." -const ( +var ( OcPrefix string = "user.oc." ) diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix_freebsd.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix_freebsd.go index cc16a696ec..daf8ffd401 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix_freebsd.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes/oc_prefix_freebsd.go @@ -23,6 +23,6 @@ package prefixes // On FreeBSD the `user` namespace is implied through a separate syscall argument // and will fail with invalid argument when you try to start an xattr name with user. or system. // For that reason we drop the superfluous user. prefix for FreeBSD specifically. -const ( +var ( OcPrefix string = "oc." ) 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 45851fa794..0cdf8f4173 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 @@ -19,6 +19,9 @@ package prefixes import ( + "fmt" + "sync" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace" ) @@ -31,7 +34,7 @@ import ( // we will use to store decomposedfs specific metadata. To prevent name // collisions with other apps We are going to introduce a sub namespace // "user.oc." in the xattrs_prefix*.go files. -const ( +var ( TypeAttr string = OcPrefix + "type" IDAttr string = OcPrefix + "id" ParentidAttr string = OcPrefix + "parentid" @@ -111,3 +114,76 @@ func FavoriteKey(uid *userpb.UserId) string { // the favorite flag is specific to the user, so we need to incorporate the userid return FavPrefix + uid.OpaqueId } + +var ( + defaultOcPrefix = OcPrefix + + mu sync.Mutex + fixed bool +) + +// SetOcPrefix sets the key prefix for the whole process, empty means the +// default. The first call fixes it, a later call with another prefix fails. +// It has to run before any key is used. +func SetOcPrefix(prefix string) error { + if prefix == "" { + prefix = defaultOcPrefix + } + + mu.Lock() + defer mu.Unlock() + + if fixed { + if prefix != OcPrefix { + return fmt.Errorf("metadata prefix is already set to %q, cannot change it to %q", OcPrefix, prefix) + } + return nil + } + + fixed = true + if prefix != OcPrefix { + setKeys(prefix) + } + return nil +} + +func setKeys(p string) { + OcPrefix = p + TypeAttr = p + "type" + IDAttr = p + "id" + ParentidAttr = p + "parentid" + OwnerIDAttr = p + "owner.id" + OwnerIDPAttr = p + "owner.idp" + OwnerTypeAttr = p + "owner.type" + NameAttr = p + "name" + BlobIDAttr = p + "blobid" + BlobsizeAttr = p + "blobsize" + StatusPrefix = p + "nodestatus" + ScanStatusPrefix = p + "scanstatus" + ScanDatePrefix = p + "scandate" + GrantPrefix = p + "grant." + GrantUserAcePrefix = p + "grant." + ace.UserAcePrefix + GrantGroupAcePrefix = p + "grant." + ace.GroupAcePrefix + GrantMailAcePrefix = p + "grant." + ace.MailAcePrefix + MetadataPrefix = p + "md." + FavPrefix = p + "fav." + TmpEtagAttr = p + "tmp.etag" + ReferenceAttr = p + "cs3.ref" + ChecksumPrefix = p + "cs." + TrashOriginAttr = p + "trash.origin" + PropagationAttr = p + "propagation" + MTimeAttr = p + "mtime" + TreeMTimeAttr = p + "tmtime" + DTimeAttr = p + "dtime" + TreesizeAttr = p + "treesize" + QuotaAttr = p + "quota" + SpaceIDAttr = p + "space.id" + SpaceNameAttr = p + "space.name" + SpaceTypeAttr = p + "space.type" + SpaceDescriptionAttr = p + "space.description" + SpaceReadmeAttr = p + "space.readme" + SpaceImageAttr = p + "space.image" + SpaceAliasAttr = p + "space.alias" + SpaceTenantIDAttr = p + "space.tenantid" + SpaceContentTypeAttr = p + "space.contenttype" +} diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options/options.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options/options.go index 32b4c67c95..f2827eccc2 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options/options.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options/options.go @@ -42,6 +42,9 @@ type Options struct { // the metadata backend to use, currently supports `xattr` or `ini` MetadataBackend string `mapstructure:"metadata_backend"` + // the attribute key prefix of the metadata on disk, process wide, see prefixes.SetOcPrefix + MetadataPrefix string `mapstructure:"metadata_prefix"` + // the propagator to use for this fs. currently only `sync` is fully supported, `async` is available as an experimental feature Propagator string `mapstructure:"propagator"` // Options specific to the async propagator diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go index fd79d52f23..135ed0de42 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -404,6 +404,11 @@ func (fs *Decomposedfs) handlePostprocessingEvent(ctx context.Context, event eve }, Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())), }, + ResourceID: &provider.ResourceId{ + StorageId: session.ProviderID(), + SpaceId: session.SpaceID(), + OpaqueId: session.NodeID(), + }, Timestamp: utils.TimeToTS(now), SpaceOwner: n.SpaceOwnerOrManager(ctx), IsVersion: isVersion, 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 index 13fdf39257..80e332fef5 100644 --- 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 @@ -2,7 +2,6 @@ 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" @@ -28,7 +27,7 @@ func (id FSSafeUserID) SafeFilename() string { // 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 base64.RawURLEncoding.EncodeToString([]byte(LowerASCII(opaqueID))) } return opaqueID } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/mail.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/mail.go new file mode 100644 index 0000000000..9cef18faf3 --- /dev/null +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/utils/mail.go @@ -0,0 +1,37 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "errors" + "net/mail" + "strings" + + "golang.org/x/net/idna" +) + +// CanonicalMail returns the canonical form of an email address. The result +// is always plain ASCII, so it can't contain look-alike characters, and two +// addresses that mail delivery treats the same end up equal: +// - only a plain address is accepted (no display name, no quoting) +// - the local part must be ASCII +// - international domains are converted to punycode +// - the result is lowercased +func CanonicalMail(email string) (string, error) { + addr, err := mail.ParseAddress(email) + if err != nil || addr.Name != "" || addr.Address != email { + return "", errors.New("invalid email address") + } + at := strings.LastIndex(email, "@") + local, domain := email[:at], email[at+1:] + if !IsASCII(local) { + return "", errors.New("email addresses with non-ASCII characters before the @ are not supported") + } + if !IsASCII(domain) { + if domain, err = idna.Lookup.ToASCII(domain); err != nil { + return "", errors.New("invalid email domain") + } + } + return strings.ToLower(local + "@" + domain), nil +} 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 78a3c960b1..ee085b6161 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 @@ -32,6 +32,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -240,11 +241,34 @@ func CanonicalUserID(id *userpb.UserId) string { // 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 LowerASCII(id.GetOpaqueId()) } return id.GetOpaqueId() } +// LowerASCII lowercases only the ASCII letters A-Z in s and leaves all other +// characters untouched. Unlike strings.ToLower it never maps a non-ASCII +// character to an ASCII one (e.g. 'İ' -> 'i' or the Kelvin sign 'K' -> 'k'), +// so two different email addresses can't end up with the same canonical form. +func LowerASCII(s string) string { + return strings.Map(func(r rune) rune { + if r >= 'A' && r <= 'Z' { + return r + ('a' - 'A') + } + return r + }, s) +} + +// IsASCII returns true if s contains only ASCII characters. +func IsASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return false + } + } + return true +} + // GroupEqual returns whether two groups have the same field values. func GroupEqual(u, v *grouppb.GroupId) bool { return u != nil && v != nil && u.Idp == v.Idp && u.OpaqueId == v.OpaqueId diff --git a/vendor/modules.txt b/vendor/modules.txt index b5750a70e0..a538cb6a79 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1360,7 +1360,7 @@ github.com/opencloud-eu/icap-client # github.com/opencloud-eu/libre-graph-api-go v1.0.8 ## explicit; go 1.23 github.com/opencloud-eu/libre-graph-api-go -# github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424 +# github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45 ## explicit; go 1.26.0 github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace github.com/opencloud-eu/reva/v2/cmd/revad/runtime