From 18d46c14166e675de769f734fd350c3e77e30fc1 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 26 Mar 2024 12:22:19 +0100 Subject: [PATCH] feat(graph): add POST /drive//root/invite suppport Partial Fix: #8351 --- .../mocks/drive_item_permissions_provider.go | 58 ++++++++ .../service/v0/api_driveitem_permissions.go | 60 +++++++- .../v0/api_driveitem_permissions_test.go | 134 +++++++++++++++++- services/graph/pkg/service/v0/service.go | 5 +- 4 files changed, 252 insertions(+), 5 deletions(-) diff --git a/services/graph/mocks/drive_item_permissions_provider.go b/services/graph/mocks/drive_item_permissions_provider.go index 0a1dca3514..9f05a266cf 100644 --- a/services/graph/mocks/drive_item_permissions_provider.go +++ b/services/graph/mocks/drive_item_permissions_provider.go @@ -82,6 +82,64 @@ func (_c *DriveItemPermissionsProvider_Invite_Call) RunAndReturn(run func(contex return _c } +// SpaceRootInvite provides a mock function with given fields: ctx, driveID, invite +func (_m *DriveItemPermissionsProvider) SpaceRootInvite(ctx context.Context, driveID providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + ret := _m.Called(ctx, driveID, invite) + + if len(ret) == 0 { + panic("no return value specified for SpaceRootInvite") + } + + var r0 libregraph.Permission + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) (libregraph.Permission, error)); ok { + return rf(ctx, driveID, invite) + } + if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) libregraph.Permission); ok { + r0 = rf(ctx, driveID, invite) + } else { + r0 = ret.Get(0).(libregraph.Permission) + } + + if rf, ok := ret.Get(1).(func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) error); ok { + r1 = rf(ctx, driveID, invite) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DriveItemPermissionsProvider_SpaceRootInvite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SpaceRootInvite' +type DriveItemPermissionsProvider_SpaceRootInvite_Call struct { + *mock.Call +} + +// SpaceRootInvite is a helper method to define mock.On call +// - ctx context.Context +// - driveID providerv1beta1.ResourceId +// - invite libregraph.DriveItemInvite +func (_e *DriveItemPermissionsProvider_Expecter) SpaceRootInvite(ctx interface{}, driveID interface{}, invite interface{}) *DriveItemPermissionsProvider_SpaceRootInvite_Call { + return &DriveItemPermissionsProvider_SpaceRootInvite_Call{Call: _e.mock.On("SpaceRootInvite", ctx, driveID, invite)} +} + +func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) Run(run func(ctx context.Context, driveID providerv1beta1.ResourceId, invite libregraph.DriveItemInvite)) *DriveItemPermissionsProvider_SpaceRootInvite_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(providerv1beta1.ResourceId), args[2].(libregraph.DriveItemInvite)) + }) + return _c +} + +func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) Return(_a0 libregraph.Permission, _a1 error) *DriveItemPermissionsProvider_SpaceRootInvite_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) RunAndReturn(run func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) (libregraph.Permission, error)) *DriveItemPermissionsProvider_SpaceRootInvite_Call { + _c.Call.Return(run) + return _c +} + // NewDriveItemPermissionsProvider creates a new instance of DriveItemPermissionsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewDriveItemPermissionsProvider(t interface { diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 505c02c457..f0d707b897 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -8,9 +8,9 @@ import ( 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" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" @@ -23,7 +23,8 @@ import ( ) type DriveItemPermissionsProvider interface { - Invite(ctx context.Context, resourceId provider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) + Invite(ctx context.Context, resourceId storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) + SpaceRootInvite(ctx context.Context, driveID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) } // DriveItemPermissionsService contains the production business logic for everything that relates to permissions on drive items. @@ -45,7 +46,7 @@ func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Sele } // Invite invites a user to a drive item. -func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId provider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { +func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { gatewayClient, err := s.gatewaySelector.Next() if err != nil { return libregraph.Permission{}, err @@ -167,6 +168,26 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId prov return *permission, nil } +// SpaceRootInvite handles invitation request on project spaces +func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return libregraph.Permission{}, err + } + + space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient) + if err != nil { + return libregraph.Permission{}, err + } + + if space.SpaceType != "project" { + return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "unsupported space type") + } + + rootResourceID := space.GetRoot() + return s.Invite(ctx, *rootResourceID, invite) +} + // DriveItemPermissionsService is the api that registers the http endpoints which expose needed operation to the graph api. // the business logic is delegated to the permissions service and further down to the cs3 client. type DriveItemPermissionsApi struct { @@ -215,3 +236,36 @@ func (api DriveItemPermissionsApi) Invite(w http.ResponseWriter, r *http.Request render.Status(r, http.StatusOK) render.JSON(w, r, &ListResponse{Value: []interface{}{permission}}) } + +func (api DriveItemPermissionsApi) SpaceRootInvite(w http.ResponseWriter, r *http.Request) { + driveID, err := parseIDParam(r, "driveID") + if err != nil { + msg := "could not parse driveID" + api.logger.Debug().Err(err).Msg(msg) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg) + return + } + + driveItemInvite := &libregraph.DriveItemInvite{} + if err = StrictJSONUnmarshal(r.Body, driveItemInvite); err != nil { + api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body") + return + } + + ctx := r.Context() + if err = validate.StructCtx(ctx, driveItemInvite); err != nil { + api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + permission, err := api.driveItemPermissionsService.SpaceRootInvite(ctx, driveID, *driveItemInvite) + + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, &ListResponse{Value: []interface{}{permission}}) +} diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index 9491189b82..7e5da750e2 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -188,10 +188,107 @@ var _ = Describe("DriveItemPermissionsService", func() { Expect(permission.GetLibreGraphPermissionsActions()).To(HaveLen(1)) Expect(permission.GetLibreGraphPermissionsActions()[0]).To(Equal(unifiedrole.DriveItemContentRead)) }) + It("fails with a missing driveritem", func() { + statResponse.Status = status.NewNotFound(context.Background(), "not found") + permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, "not found"))) + Expect(permission).To(BeZero()) + }) + }) + Describe("SpaceRootInvite", func() { + var ( + listSpacesResponse *provider.ListStorageSpacesResponse + createShareResponse *collaboration.CreateShareResponse + driveItemInvite libregraph.DriveItemInvite + driveId provider.ResourceId + statResponse *provider.StatResponse + getUserResponse *userpb.GetUserResponse + ) + + BeforeEach(func() { + driveId = provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + } + ctx := revactx.ContextSetUser(context.Background(), currentUser) + + statResponse = &provider.StatResponse{ + Status: status.NewOK(ctx), + } + + listSpacesResponse = &provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*provider.StorageSpace{ + { + Id: &provider.StorageSpaceId{ + OpaqueId: "2", + }, + }, + }, + } + + getUserResponse = &userpb.GetUserResponse{ + Status: status.NewOK(ctx), + User: &userpb.User{ + Id: &userpb.UserId{OpaqueId: "1"}, + DisplayName: "Cem Kaner", + }, + } + + createShareResponse = &collaboration.CreateShareResponse{ + Status: status.NewOK(ctx), + } + }) + + It("adds a user to a space as expected (happy path)", func() { + listSpacesResponse.StorageSpaces[0].SpaceType = "project" + listSpacesResponse.StorageSpaces[0].Root = &provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + } + + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil) + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) + gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, + } + driveItemInvite.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(time.Hour)) + createShareResponse.Share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "123"}, + Expiration: utils.TimeToTS(*driveItemInvite.ExpirationDateTime), + } + + permission, err := driveItemPermissionsService.SpaceRootInvite(context.Background(), driveId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + Expect(permission.GetId()).To(Equal("123")) + Expect(permission.GetExpirationDateTime().Equal(*driveItemInvite.ExpirationDateTime)).To(BeTrue()) + Expect(permission.GrantedToV2.User.GetDisplayName()).To(Equal(getUserResponse.User.DisplayName)) + Expect(permission.GrantedToV2.User.GetId()).To(Equal("1")) + }) + It("rejects to add a user to a personal space", func() { + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, + } + driveItemInvite.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(time.Hour)) + createShareResponse.Share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "123"}, + Expiration: utils.TimeToTS(*driveItemInvite.ExpirationDateTime), + } + + permission, err := driveItemPermissionsService.SpaceRootInvite(context.Background(), driveId, driveItemInvite) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, "unsupported space type"))) + Expect(permission).To(BeZero()) + }) }) }) -var _ = Describe("DriveItemPermissionsApiApi", func() { +var _ = Describe("DriveItemPermissionsApi", func() { var ( mockProvider *mocks.DriveItemPermissionsProvider httpAPI svc.DriveItemPermissionsApi @@ -286,4 +383,39 @@ var _ = Describe("DriveItemPermissionsApiApi", func() { Expect(responseRecorder.Code).To(Equal(http.StatusOK)) }) }) + Describe("SpaceRootInvite", func() { + It("call the Invite provider with the correct arguments", func() { + responseRecorder := httptest.NewRecorder() + inviteJson, err := json.Marshal(invite) + Expect(err).ToNot(HaveOccurred()) + + onInvite := mockProvider.On("SpaceRootInvite", mock.Anything, mock.Anything, mock.Anything) + onInvite.Return(func(ctx context.Context, driveID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + Expect(storagespace.FormatResourceID(driveID)).To(Equal("1$2")) + return libregraph.Permission{}, nil + }).Once() + + request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + httpAPI.SpaceRootInvite(responseRecorder, request) + + Expect(responseRecorder.Code).To(Equal(http.StatusOK)) + }) + It("call the Invite provider with the correct arguments", func() { + rCTX.URLParams.Add("driveID", "") + responseRecorder := httptest.NewRecorder() + inviteJson, err := json.Marshal(invite) + Expect(err).ToNot(HaveOccurred()) + + request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + httpAPI.SpaceRootInvite(responseRecorder, request) + + Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity)) + }) + }) }) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index b12f231b7a..ea7a90b820 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -237,7 +237,10 @@ func NewService(opts ...Option) (Graph, error) { r.Route("/drives", func(r chi.Router) { r.Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) r.Route("/{driveID}", func(r chi.Router) { - r.Post("/root/children", drivesDriveItemApi.CreateDriveItem) + r.Route("/root", func(r chi.Router) { + r.Post("/children", drivesDriveItemApi.CreateDriveItem) + r.Post("/invite", driveItemPermissionsApi.SpaceRootInvite) + }) r.Route("/items/{itemID}", func(r chi.Router) { r.Delete("/", drivesDriveItemApi.DeleteDriveItem) r.Post("/invite", driveItemPermissionsApi.Invite)