mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 21:58:58 -04:00
graph: expose @libre.graph.shareTypes on driveItems
This commit is contained in:
1 parent
61037cc22f
commit
aa968ec898
6 files changed
+240
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.<facet>." key prefix. Nil when no such keys are present.
|
||||
func metadataToFacet[T any](metadata map[string]string, facet string) *T {
|
||||
|
||||
@@ -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),
|
||||
|
||||
+37
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user