diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index f9ae229da8..8307c3d200 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -24,12 +24,29 @@ 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" "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. @@ -211,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}) } @@ -275,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) } @@ -457,6 +484,12 @@ 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") + 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 f36f6c06fd..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, + )) }) }) @@ -293,6 +331,71 @@ 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()) + 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() { + 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)