From 9fc8ec4fa3fad5e4ac13b07ddad9af4d64da069f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Jul 2026 12:31:43 +0200 Subject: [PATCH 1/3] graph: expose @libre.graph.me.following on driveItems --- services/graph/pkg/service/v0/driveitems.go | 3 ++ .../graph/pkg/service/v0/driveitems_test.go | 42 +++++++++++++++++++ services/graph/pkg/service/v0/follow.go | 7 +++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index f9ae229da8..d25d20f533 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -457,6 +457,9 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto driveItem.Image = cs3ResourceToDriveItemImageFacet(logger, res) driveItem.Location = cs3ResourceToDriveItemLocationFacet(logger, res) driveItem.Photo = cs3ResourceToDriveItemPhotoFacet(logger, res) + + m := res.GetArbitraryMetadata().GetMetadata() + driveItem.LibreGraphMeFollowing = libregraph.PtrBool(m[_favoriteMetadataKey] == "1") } return driveItem, nil diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index f36f6c06fd..5174302a97 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -293,6 +293,48 @@ var _ = Describe("Driveitems", func() { res := assertItemsList(1) Expect(res.Value[0].Audio).To(BeNil()) Expect(res.Value[0].Location).To(BeNil()) + Expect(res.Value[0].LibreGraphMeFollowing).To(BeNil()) + }) + + It("returns the following state if metadata is available", func() { + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(mtime), + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: map[string]string{ + "http://owncloud.org/ns/favorite": "1", + }, + }, + }, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].GetLibreGraphMeFollowing()).To(BeTrue()) + }) + + It("reports not following if the favorite flag is absent", func() { + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(mtime), + ArbitraryMetadata: &provider.ArbitraryMetadata{Metadata: map[string]string{}}, + }, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].LibreGraphMeFollowing).ToNot(BeNil()) + Expect(res.Value[0].GetLibreGraphMeFollowing()).To(BeFalse()) }) It("returns the audio facet if metadata is available", func() { diff --git a/services/graph/pkg/service/v0/follow.go b/services/graph/pkg/service/v0/follow.go index 7a255451a1..d2866982f3 100644 --- a/services/graph/pkg/service/v0/follow.go +++ b/services/graph/pkg/service/v0/follow.go @@ -12,7 +12,11 @@ import ( "github.com/opencloud-eu/reva/v2/pkg/events" ) -const _favoriteLabel = "favorite" +const ( + _favoriteLabel = "favorite" + // deliberately not using the existing node.FavoriteKey constant to avoid importing a specific storage driver package + _favoriteMetadataKey = "http://owncloud.org/ns/favorite" +) // FollowDriveItem marks a drive item as favorite. func (g Graph) FollowDriveItem(w http.ResponseWriter, r *http.Request) { @@ -99,6 +103,7 @@ func (g Graph) FollowDriveItem(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } + driveItem.SetLibreGraphMeFollowing(true) render.Status(r, http.StatusCreated) render.JSON(w, r, &driveItem) From eb75fc0cf4a96d1298086fd047421647171d9ecc Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Jul 2026 12:32:09 +0200 Subject: [PATCH 2/3] graph: expose @libre.graph.tags on driveItems --- services/graph/pkg/service/v0/driveitems.go | 4 ++++ .../graph/pkg/service/v0/driveitems_test.go | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index d25d20f533..87f34f3e64 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -24,6 +24,7 @@ import ( revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/storagespace" + "github.com/opencloud-eu/reva/v2/pkg/tags" "github.com/opencloud-eu/reva/v2/pkg/utils" "github.com/opencloud-eu/opencloud/pkg/log" @@ -460,6 +461,9 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto m := res.GetArbitraryMetadata().GetMetadata() driveItem.LibreGraphMeFollowing = libregraph.PtrBool(m[_favoriteMetadataKey] == "1") + if t := m["tags"]; t != "" { + driveItem.LibreGraphTags = tags.New(t).AsSlice() + } } return driveItem, nil diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 5174302a97..b32530fef7 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -294,6 +294,29 @@ var _ = Describe("Driveitems", func() { Expect(res.Value[0].Audio).To(BeNil()) Expect(res.Value[0].Location).To(BeNil()) Expect(res.Value[0].LibreGraphMeFollowing).To(BeNil()) + Expect(res.Value[0].LibreGraphTags).To(BeNil()) + }) + + It("returns tags if metadata is available", func() { + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(mtime), + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: map[string]string{ + "tags": "marketing,important", + }, + }, + }, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].GetLibreGraphTags()).To(ConsistOf("marketing", "important")) }) It("returns the following state if metadata is available", func() { From 21cbc2a748d76c9620cd64db207103666a49c675 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Jul 2026 16:55:21 +0200 Subject: [PATCH 3/3] graph: expose @libre.graph.permissions.actions.allowedValues on driveItems Opt-in via $select, on GetDriveItem and the root children listing. --- services/graph/pkg/service/v0/driveitems.go | 26 +++++++++++++ .../graph/pkg/service/v0/driveitems_test.go | 38 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 87f34f3e64..8307c3d200 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -29,8 +29,24 @@ 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/unifiedrole" ) +// opt-in driveItem instance annotations, returned only when requested via $select +const _selectAllowedValues = "@libre.graph.permissions.actions.allowedValues" + +// driveItemPropertySelected reports whether the given opt-in property was requested via $select +func driveItemPropertySelected(r *http.Request, property string) bool { + for _, values := range r.URL.Query()["$select"] { + for _, v := range strings.Split(values, ",") { + if v == property { + return true + } + } + } + return false +} + // CreateUploadSession create an upload session to allow your app to upload files up to the maximum file size. // An upload session allows your app to upload ranges of the file in sequential API requests, which allows the // transfer to be resumed if a connection is dropped while the upload is in progress. @@ -212,6 +228,12 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { return } + if driveItemPropertySelected(r, _selectAllowedValues) { + for i, info := range lRes.GetInfos() { + files[i].LibreGraphPermissionsActionsAllowedValues = unifiedrole.CS3ResourcePermissionsToLibregraphActions(info.GetPermissionSet()) + } + } + render.Status(r, http.StatusOK) render.JSON(w, r, &ListResponse{Value: files}) } @@ -276,6 +298,10 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) { return } + if driveItemPropertySelected(r, _selectAllowedValues) { + driveItem.LibreGraphPermissionsActionsAllowedValues = unifiedrole.CS3ResourcePermissionsToLibregraphActions(res.GetInfo().GetPermissionSet()) + } + render.Status(r, http.StatusOK) render.JSON(w, r, &driveItem) } diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index b32530fef7..b588638589 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -31,6 +31,7 @@ import ( "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks" service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0" + "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" ) type itemsList struct { @@ -196,6 +197,43 @@ var _ = Describe("Driveitems", func() { Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue()) Expect(res.Value[0].GetETag()).To(Equal("etag")) Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid")) + Expect(res.Value[0].LibreGraphPermissionsActionsAllowedValues).To(BeNil()) + }) + + It("returns the allowed actions when requested via $select", func() { + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}}, + }, nil) + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(time.Now()), + PermissionSet: &provider.ResourcePermissions{ + GetPath: true, + InitiateFileDownload: true, + }, + }, + }, + }, nil) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children?$select=@libre.graph.permissions.actions.allowedValues", nil) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.GetRootDriveChildren(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + Expect(json.Unmarshal(data, &res)).To(Succeed()) + Expect(len(res.Value)).To(Equal(1)) + Expect(res.Value[0].GetLibreGraphPermissionsActionsAllowedValues()).To(ConsistOf( + unifiedrole.DriveItemPathRead, + unifiedrole.DriveItemContentRead, + )) }) })