Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
105 changes: 100 additions & 5 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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})
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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())
Expand All @@ -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
}

Expand Down Expand Up @@ -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()))
Comment thread
dschmidt marked this conversation as resolved.
}

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.<facet>." key prefix. Nil when no such keys are present.
func metadataToFacet[T any](metadata map[string]string, facet string) *T {
Expand Down
99 changes: 99 additions & 0 deletions services/graph/pkg/service/v0/driveitems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down