graph: Initial implementation of /me/drives/sharedByMe endpoint

This adds a still incomplete implementation of the new /me/drives/sharedByMe
endpoint. For now it only sets the driveItem Id property. The 'permissions' relation
is not supported yet.

This endpoint is added to the /v1beta1 route, to indicate that it is
still imcomplete and might require changes.
This commit is contained in:
Ralf Haferkamp
2023-09-05 16:27:54 +02:00
committed by Ralf Haferkamp
parent 86b061421e
commit f18775b1cb
3 changed files with 460 additions and 1 deletions

View File

@@ -96,6 +96,7 @@ type Service interface {
GetDrives(w http.ResponseWriter, r *http.Request)
GetSingleDrive(w http.ResponseWriter, r *http.Request)
GetAllDrives(w http.ResponseWriter, r *http.Request)
GetSharedByMe(w http.ResponseWriter, r *http.Request)
CreateDrive(w http.ResponseWriter, r *http.Request)
UpdateDrive(w http.ResponseWriter, r *http.Request)
DeleteDrive(w http.ResponseWriter, r *http.Request)
@@ -199,6 +200,9 @@ func NewService(opts ...Option) (Graph, error) {
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Use(middleware.StripSlashes)
r.Route("/v1beta1", func(r chi.Router) {
r.Get("/me/drives/sharedByMe", svc.GetSharedByMe)
})
r.Route("/v1.0", func(r chi.Router) {
r.Route("/extensions/org.libregraph", func(r chi.Router) {
r.Get("/tags", svc.GetTags)
@@ -212,7 +216,9 @@ func NewService(opts ...Option) (Graph, error) {
r.Route("/me", func(r chi.Router) {
r.Get("/", svc.GetMe)
r.Get("/drive", svc.GetUserDrive)
r.Get("/drives", svc.GetDrives)
r.Route("/drives", func(r chi.Router) {
r.Get("/", svc.GetDrives)
})
r.Get("/drive/root/children", svc.GetRootDriveChildren)
r.Post("/changePassword", svc.ChangeOwnPassword)
})

View File

@@ -0,0 +1,149 @@
package svc
import (
"context"
"net/http"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
type driveItemsByResourceID map[string]libregraph.DriveItem
// GetSharedByMe implements the Service interface (/me/drives/sharedByMe endpoint)
func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) {
g.logger.Debug().Msg("Calling GetRootDriveChildren")
ctx := r.Context()
driveItems := make(driveItemsByResourceID)
var err error
driveItems, err = g.listUserShares(ctx, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
_, err = g.listPublicShares(ctx, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
res := make([]libregraph.DriveItem, 0, len(driveItems))
for _, v := range driveItems {
res = append(res, v)
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: res})
}
func (g Graph) listUserShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
filters := []*collaboration.Filter{
share.UserGranteeFilter(),
share.GroupGranteeFilter(),
}
lsUserSharesRequest := collaboration.ListSharesRequest{
Filters: filters,
}
lsUserSharesResponse, err := gatewayClient.ListShares(ctx, &lsUserSharesRequest)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsUserSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsUserSharesResponse.Status.Message)
}
driveItems, err = g.cs3UserSharesToDriveItems(ctx, lsUserSharesResponse.Shares, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g Graph) listPublicShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
filters := []*link.ListPublicSharesRequest_Filter{}
req := link.ListPublicSharesRequest{
Filters: filters,
}
lsPublicSharesResponse, err := gatewayClient.ListPublicShares(ctx, &req)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsPublicSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsPublicSharesResponse.Status.Message)
}
driveItems, err = g.cs3PublicSharesToDriveItems(ctx, lsPublicSharesResponse.Share, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g Graph) cs3UserSharesToDriveItems(ctx context.Context, shares []*collaboration.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
for _, s := range shares {
g.logger.Debug().Interface("CS3 UserShare", s).Msg("Got Share")
resIDStr := storagespace.FormatResourceID(*s.ResourceId)
item, ok := driveItems[resIDStr]
if !ok {
item = libregraph.DriveItem{
Id: libregraph.PtrString(resIDStr),
}
}
driveItems[resIDStr] = item
}
return driveItems, nil
}
func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
for _, s := range shares {
g.logger.Debug().Interface("CS3 PublicShare", s).Msg("Got Share")
resIDStr := storagespace.FormatResourceID(*s.ResourceId)
item, ok := driveItems[resIDStr]
if !ok {
item = libregraph.DriveItem{
Id: libregraph.PtrString(resIDStr),
}
}
driveItems[resIDStr] = item
}
return driveItems, nil
}
func cs3StatusToErrCode(code rpc.Code) (errcode errorcode.ErrorCode) {
switch code {
case rpc.Code_CODE_UNAUTHENTICATED:
errcode = errorcode.Unauthenticated
case rpc.Code_CODE_PERMISSION_DENIED:
errcode = errorcode.AccessDenied
case rpc.Code_CODE_NOT_FOUND:
errcode = errorcode.ItemNotFound
default:
errcode = errorcode.GeneralException
}
return errcode
}

View File

@@ -0,0 +1,304 @@
package svc_test
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
)
var _ = Describe("Driveitems", func() {
var (
svc service.Service
ctx context.Context
cfg *config.Config
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
eventsPublisher mocks.Publisher
identityBackend *identitymocks.Backend
rr *httptest.ResponseRecorder
newGroup *libregraph.Group
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
rr = httptest.NewRecorder()
pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(
&link.ListPublicSharesResponse{
Status: status.NewOK(ctx),
Share: []*link.PublicShare{},
},
nil,
)
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"com.owncloud.api.gateway",
func(cc *grpc.ClientConn) gateway.GatewayAPIClient {
return gatewayClient
},
)
identityBackend = &identitymocks.Backend{}
newGroup = libregraph.NewGroup()
newGroup.SetMembersodataBind([]string{"/users/user1"})
newGroup.SetId("group1")
rr = httptest.NewRecorder()
ctx = context.Background()
cfg = defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
svc, _ = service.NewService(
service.Config(cfg),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
)
})
Describe("GetSharedByMe", func() {
expiration := time.Now()
userShare := collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: "share-id",
},
ResourceId: &provider.ResourceId{
StorageId: "storageid",
SpaceId: "spaceid",
OpaqueId: "opaqueid",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: "user-id",
},
},
},
}
groupShare := collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: "share-id",
},
ResourceId: &provider.ResourceId{
StorageId: "storageid",
SpaceId: "spaceid",
OpaqueId: "opaqueid",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
Id: &provider.Grantee_GroupId{
GroupId: &grouppb.GroupId{
OpaqueId: "group-id",
},
},
},
}
userShareWithExpiration := collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: "expire-share-id",
},
ResourceId: &provider.ResourceId{
StorageId: "storageid",
SpaceId: "spaceid",
OpaqueId: "expire-opaqueid",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: "user-id",
},
},
},
Expiration: utils.TimeToTS(expiration),
}
It("handles a failing ListShares", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(nil, errors.New("some error"))
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
})
It("handles ListShares returning an error status", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{Status: status.NewInternal(ctx, "error listing shares")},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
})
It("succeeds, when no shares are returned", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{},
},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(0))
})
It("returns a proper driveItem, when a single user share is returned", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{
&userShare,
},
},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
di := res.Value[0]
Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShare.GetResourceId())))
})
It("returns a proper driveItem, when a single group share is returned", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{
&groupShare,
},
},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
di := res.Value[0]
Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId())))
})
It("returns a single driveItem, when a mulitple shares for the same resource are returned", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{
&groupShare,
&userShare,
},
},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
di := res.Value[0]
Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId())))
})
It("return a driveItem with the expiration date set, for expiring shares", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
&collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{
&userShareWithExpiration,
},
},
nil,
)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
svc.GetSharedByMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
di := res.Value[0]
Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShareWithExpiration.GetResourceId())))
})
})
})