From aa968ec898c8819cf3dd6db656dbc2ae5b382b15 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 1 Sep 2026 14:00:37 +0200 Subject: [PATCH] graph: expose @libre.graph.shareTypes on driveItems --- go.mod | 2 +- go.sum | 2 + services/graph/pkg/service/v0/driveitems.go | 105 +++++++++++++++++- .../graph/pkg/service/v0/driveitems_test.go | 99 +++++++++++++++++ .../libre-graph-api-go/model_drive_item.go | 37 ++++++ vendor/modules.txt | 2 +- 6 files changed, 240 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index ba7145150c..8c65fdbf3a 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/onsi/gomega v1.42.1 github.com/open-policy-agent/opa v1.19.1 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 - github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901070651-08a5330ce57d + github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901115715-6c4a98340429 github.com/opencloud-eu/reva/v2 v2.49.0 github.com/opensearch-project/opensearch-go/v4 v4.7.3 github.com/orcaman/concurrent-map v1.0.0 diff --git a/go.sum b/go.sum index 0bf183c787..1b63c2319b 100644 --- a/go.sum +++ b/go.sum @@ -942,6 +942,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-0.20260901070651-08a5330ce57d h1:3zbb31655ZvqwLMWMIiUAhYer6YcFnv/oguujnX7aOU= github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901070651-08a5330ce57d/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901115715-6c4a98340429 h1:i/C5/1I0eRkjOcdtqQboyPvV0bPyAOahe/wWHGupKVY= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901115715-6c4a98340429/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU= github.com/opencloud-eu/reva/v2 v2.49.0 h1:AwECMDDth3NUaihZRf9bI9HNpWutvaRijOCvQyQfTT4= github.com/opencloud-eu/reva/v2 v2.49.0/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI= diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 00350ec516..2f282dba40 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -15,13 +15,16 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/go-chi/render" libregraph "github.com/opencloud-eu/libre-graph-api-go" "golang.org/x/crypto/sha3" + "google.golang.org/protobuf/types/known/fieldmaskpb" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/publicshare" "github.com/opencloud-eu/reva/v2/pkg/storagespace" "github.com/opencloud-eu/reva/v2/pkg/tags" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -33,7 +36,13 @@ import ( ) // opt-in driveItem instance annotations, returned only when requested via $select -const _selectAllowedValues = "@libre.graph.permissions.actions.allowedValues" +const ( + _selectAllowedValues = "@libre.graph.permissions.actions.allowedValues" + _selectShareTypes = "@libre.graph.shareTypes" +) + +// without it the provider leaves the share-types opaque empty +var shareTypesFieldMask = &fieldmaskpb.FieldMask{Paths: []string{"share-types"}} // opt-in driveItem relations, returned only when requested via $expand const _expandChildren = "children" @@ -210,9 +219,14 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { } } - lRes, err := gatewayClient.ListContainer(ctx, &storageprovider.ListContainerRequest{ + listRequest := &storageprovider.ListContainerRequest{ Ref: &storageprovider.Reference{ResourceId: space.GetRoot()}, - }) + } + if driveItemPropertySelected(r, _selectShareTypes) { + listRequest.FieldMask = shareTypesFieldMask + } + + lRes, err := gatewayClient.ListContainer(ctx, listRequest) switch { case err != nil: g.logger.Error().Err(err).Msg("error making ListContainer grpc call") @@ -246,6 +260,10 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { } } + if driveItemPropertySelected(r, _selectShareTypes) { + g.addShareTypes(ctx, files, lRes.GetInfos()) + } + render.Status(r, http.StatusOK) render.JSON(w, r, &ListResponse{Value: files}) } @@ -323,6 +341,11 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) { driveItem.Children = children } + if driveItemPropertySelected(r, _selectShareTypes) { + infos := []*storageprovider.ResourceInfo{res.GetInfo()} + driveItem.LibreGraphShareTypes = shareTypesOf(res.GetInfo(), g.listLinkShares(ctx, infos)) + } + render.Status(r, http.StatusOK) render.JSON(w, r, &driveItem) } @@ -371,9 +394,14 @@ func (g Graph) listDriveItemChildren(w http.ResponseWriter, r *http.Request, dri return nil, false } - res, err := gatewayClient.ListContainer(r.Context(), &storageprovider.ListContainerRequest{ + childrenRequest := &storageprovider.ListContainerRequest{ Ref: &storageprovider.Reference{ResourceId: driveItemID}, - }) + } + if driveItemPropertySelected(r, _selectShareTypes) { + childrenRequest.FieldMask = shareTypesFieldMask + } + + res, err := gatewayClient.ListContainer(r.Context(), childrenRequest) switch { case err != nil: errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) @@ -400,6 +428,10 @@ func (g Graph) listDriveItemChildren(w http.ResponseWriter, r *http.Request, dri return nil, false } + if driveItemPropertySelected(r, _selectShareTypes) { + g.addShareTypes(r.Context(), files, res.GetInfos()) + } + return files, true } @@ -532,6 +564,69 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto return driveItem, nil } +// addShareTypes reads user and group shares off the grants, links from the share +// manager. Links are not stored as grants, so they need one filter per item, +// which is why the whole annotation is only built when it is selected. +func (g Graph) addShareTypes(ctx context.Context, items []libregraph.DriveItem, infos []*storageprovider.ResourceInfo) { + linkShares := g.listLinkShares(ctx, infos) + + for i, info := range infos { + if i >= len(items) { + break + } + + items[i].LibreGraphShareTypes = shareTypesOf(info, linkShares) + } +} + +// shareTypesOf maps the grants on a resource, plus a hit in the link lookup, to +// the annotation values. +func shareTypesOf(info *storageprovider.ResourceInfo, linkShares map[string]struct{}) []string { + var types []string + for _, grant := range strings.Split(utils.ReadPlainFromOpaque(info.GetOpaque(), "share-types"), ",") { + switch grant { + case strconv.Itoa(int(storageprovider.GranteeType_GRANTEE_TYPE_USER)): + types = append(types, "user") + case strconv.Itoa(int(storageprovider.GranteeType_GRANTEE_TYPE_GROUP)): + types = append(types, "group") + } + } + + if _, ok := linkShares[info.GetId().GetOpaqueId()]; ok { + types = append(types, "link") + } + + return types +} + +// listLinkShares returns the resource ids that carry a public link. A failed +// lookup is logged and treated as "no links", same as the WebDAV PROPFIND. +func (g Graph) listLinkShares(ctx context.Context, infos []*storageprovider.ResourceInfo) map[string]struct{} { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Error().Err(err).Msg("could not select gateway client for public shares") + return nil + } + + filters := make([]*link.ListPublicSharesRequest_Filter, 0, len(infos)) + for _, info := range infos { + filters = append(filters, publicshare.ResourceIDFilter(info.GetId())) + } + + res, err := gatewayClient.ListPublicShares(ctx, &link.ListPublicSharesRequest{Filters: filters}) + if err != nil || res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK { + g.logger.Error().Err(err).Msg("could not list public shares") + return nil + } + + linkShares := make(map[string]struct{}, len(res.GetShare())) + for _, share := range res.GetShare() { + linkShares[share.GetResourceId().GetOpaqueId()] = struct{}{} + } + + return linkShares +} + // metadataToFacet builds a DriveItem facet *T from CS3 arbitrary metadata under // the "libre.graph.." key prefix. Nil when no such keys are present. func metadataToFacet[T any](metadata map[string]string, facet string) *T { diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 369b35e9ee..ec12536933 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -11,6 +11,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" @@ -436,6 +437,104 @@ var _ = Describe("Driveitems", func() { Expect(res.Value[0].PendingOperations).To(BeNil()) }) + It("omits share types unless they are selected", 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), + Opaque: utils.AppendPlainToOpaque(nil, "share-types", "1,2"), + }, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].LibreGraphShareTypes).To(BeNil()) + gatewayClient.AssertNotCalled(GinkgoT(), "ListPublicShares", mock.Anything, mock.Anything) + }) + + It("returns the share types of an item when selected", func() { + r = r.WithContext(r.Context()) + q := r.URL.Query() + q.Add("$select", "@libre.graph.shareTypes") + r.URL.RawQuery = q.Encode() + + 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), + Opaque: utils.AppendPlainToOpaque(nil, "share-types", "1,2"), + }, + }, + }, nil) + gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(&link.ListPublicSharesResponse{ + Status: status.NewOK(ctx), + Share: []*link.PublicShare{ + {ResourceId: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}}, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].LibreGraphShareTypes).To(ConsistOf("user", "group", "link")) + }) + + It("reports only the link when the item has no grants", func() { + q := r.URL.Query() + q.Add("$select", "@libre.graph.shareTypes") + r.URL.RawQuery = q.Encode() + + 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), + }, + }, + }, nil) + gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(&link.ListPublicSharesResponse{ + Status: status.NewOK(ctx), + Share: []*link.PublicShare{ + {ResourceId: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}}, + }, + }, nil) + + res := assertItemsList(1) + Expect(res.Value[0].LibreGraphShareTypes).To(ConsistOf("link")) + }) + + It("keeps the grant types when the public share lookup fails", func() { + q := r.URL.Query() + q.Add("$select", "@libre.graph.shareTypes") + r.URL.RawQuery = q.Encode() + + 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), + Opaque: utils.AppendPlainToOpaque(nil, "share-types", "1"), + }, + }, + }, nil) + gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(nil, errors.New("nope")) + + res := assertItemsList(1) + Expect(res.Value[0].LibreGraphShareTypes).To(ConsistOf("user")) + }) + It("reports a pending content update while the item is being processed", func() { gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ Status: status.NewOK(ctx), diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go index bf74ea8963..07e36220fc 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go @@ -80,6 +80,8 @@ type DriveItem struct { LibreGraphTags []string `json:"@libre.graph.tags,omitempty"` // A list of actions the caller is allowed to perform on this item. Only returned when explicitly requested via `$select` on endpoints that support it. Mirrors the annotation of the same name on the `/permissions` endpoint, allowing clients to learn a caller's effective actions on an item without a separate round-trip. LibreGraphPermissionsActionsAllowedValues []string `json:"@libre.graph.permissions.actions.allowedValues,omitempty"` + // The types of shares existing on this item, aggregated over all of its grants. Absent or empty if the item is not shared. This is a summary of the item's `permissions` collection. For the full grants use the permissions endpoints, for the caller's own capabilities use `@libre.graph.permissions.actions.allowedValues`. Only returned when explicitly requested via `$select`. + LibreGraphShareTypes []string `json:"@libre.graph.shareTypes,omitempty"` } // NewDriveItem instantiates a new DriveItem object @@ -1347,6 +1349,38 @@ func (o *DriveItem) SetLibreGraphPermissionsActionsAllowedValues(v []string) { o.LibreGraphPermissionsActionsAllowedValues = v } +// GetLibreGraphShareTypes returns the LibreGraphShareTypes field value if set, zero value otherwise. +func (o *DriveItem) GetLibreGraphShareTypes() []string { + if o == nil || IsNil(o.LibreGraphShareTypes) { + var ret []string + return ret + } + return o.LibreGraphShareTypes +} + +// GetLibreGraphShareTypesOk returns a tuple with the LibreGraphShareTypes field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveItem) GetLibreGraphShareTypesOk() ([]string, bool) { + if o == nil || IsNil(o.LibreGraphShareTypes) { + return nil, false + } + return o.LibreGraphShareTypes, true +} + +// HasLibreGraphShareTypes returns a boolean if a field has been set. +func (o *DriveItem) HasLibreGraphShareTypes() bool { + if o != nil && !IsNil(o.LibreGraphShareTypes) { + return true + } + + return false +} + +// SetLibreGraphShareTypes gets a reference to the given []string and assigns it to the LibreGraphShareTypes field. +func (o *DriveItem) SetLibreGraphShareTypes(v []string) { + o.LibreGraphShareTypes = v +} + func (o DriveItem) MarshalJSON() ([]byte, error) { toSerialize,err := o.ToMap() if err != nil { @@ -1474,6 +1508,9 @@ func (o DriveItem) ToMap() (map[string]interface{}, error) { if !IsNil(o.LibreGraphPermissionsActionsAllowedValues) { toSerialize["@libre.graph.permissions.actions.allowedValues"] = o.LibreGraphPermissionsActionsAllowedValues } + if !IsNil(o.LibreGraphShareTypes) { + toSerialize["@libre.graph.shareTypes"] = o.LibreGraphShareTypes + } return toSerialize, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 0bb6c97bcc..421f0afe74 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1364,7 +1364,7 @@ github.com/open-policy-agent/opa/v1/version # github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 ## explicit; go 1.24.6 github.com/opencloud-eu/icap-client -# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901070651-08a5330ce57d +# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260901115715-6c4a98340429 ## explicit; go 1.23 github.com/opencloud-eu/libre-graph-api-go # github.com/opencloud-eu/reva/v2 v2.49.0