diff --git a/services/graph/.mockery.yaml b/services/graph/.mockery.yaml index a4a3d774b..86bb59dd8 100644 --- a/services/graph/.mockery.yaml +++ b/services/graph/.mockery.yaml @@ -8,6 +8,7 @@ packages: dir: "mocks" interfaces: DrivesDriveItemProvider: + DriveItemPermissionsProvider: HTTPClient: Permissions: Publisher: diff --git a/services/graph/mocks/drive_item_permissions_provider.go b/services/graph/mocks/drive_item_permissions_provider.go new file mode 100644 index 000000000..0a1dca351 --- /dev/null +++ b/services/graph/mocks/drive_item_permissions_provider.go @@ -0,0 +1,97 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + libregraph "github.com/owncloud/libre-graph-api-go" + mock "github.com/stretchr/testify/mock" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +// DriveItemPermissionsProvider is an autogenerated mock type for the DriveItemPermissionsProvider type +type DriveItemPermissionsProvider struct { + mock.Mock +} + +type DriveItemPermissionsProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *DriveItemPermissionsProvider) EXPECT() *DriveItemPermissionsProvider_Expecter { + return &DriveItemPermissionsProvider_Expecter{mock: &_m.Mock} +} + +// Invite provides a mock function with given fields: ctx, resourceId, invite +func (_m *DriveItemPermissionsProvider) Invite(ctx context.Context, resourceId providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + ret := _m.Called(ctx, resourceId, invite) + + if len(ret) == 0 { + panic("no return value specified for Invite") + } + + 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, resourceId, invite) + } + if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) libregraph.Permission); ok { + r0 = rf(ctx, resourceId, 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, resourceId, invite) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DriveItemPermissionsProvider_Invite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Invite' +type DriveItemPermissionsProvider_Invite_Call struct { + *mock.Call +} + +// Invite is a helper method to define mock.On call +// - ctx context.Context +// - resourceId providerv1beta1.ResourceId +// - invite libregraph.DriveItemInvite +func (_e *DriveItemPermissionsProvider_Expecter) Invite(ctx interface{}, resourceId interface{}, invite interface{}) *DriveItemPermissionsProvider_Invite_Call { + return &DriveItemPermissionsProvider_Invite_Call{Call: _e.mock.On("Invite", ctx, resourceId, invite)} +} + +func (_c *DriveItemPermissionsProvider_Invite_Call) Run(run func(ctx context.Context, resourceId providerv1beta1.ResourceId, invite libregraph.DriveItemInvite)) *DriveItemPermissionsProvider_Invite_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_Invite_Call) Return(_a0 libregraph.Permission, _a1 error) *DriveItemPermissionsProvider_Invite_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DriveItemPermissionsProvider_Invite_Call) RunAndReturn(run func(context.Context, providerv1beta1.ResourceId, libregraph.DriveItemInvite) (libregraph.Permission, error)) *DriveItemPermissionsProvider_Invite_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 { + mock.TestingT + Cleanup(func()) +}) *DriveItemPermissionsProvider { + mock := &DriveItemPermissionsProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/mocks/drives_drive_item_provider.go b/services/graph/mocks/drives_drive_item_provider.go index e3aa0cc13..1442ef359 100644 --- a/services/graph/mocks/drives_drive_item_provider.go +++ b/services/graph/mocks/drives_drive_item_provider.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package mocks diff --git a/services/graph/mocks/gateway_selector.go b/services/graph/mocks/gateway_selector.go index aa66401b1..eedc1f499 100644 --- a/services/graph/mocks/gateway_selector.go +++ b/services/graph/mocks/gateway_selector.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package mocks diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go new file mode 100644 index 000000000..505c02c45 --- /dev/null +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -0,0 +1,217 @@ +package svc + +import ( + "context" + "net/http" + + 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" + 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/utils" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/conversions" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/identity" + "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" + "github.com/owncloud/ocis/v2/services/graph/pkg/validate" +) + +type DriveItemPermissionsProvider interface { + Invite(ctx context.Context, resourceId provider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) +} + +// DriveItemPermissionsService contains the production business logic for everything that relates to permissions on drive items. +type DriveItemPermissionsService struct { + logger log.Logger + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + identityCache identity.IdentityCache + resharingEnabled bool +} + +// NewDriveItemPermissionsService creates a new DriveItemPermissionsService +func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, resharing bool) (DriveItemPermissionsService, error) { + return DriveItemPermissionsService{ + logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()}, + gatewaySelector: gatewaySelector, + identityCache: identityCache, + resharingEnabled: resharing, + }, nil +} + +// Invite invites a user to a drive item. +func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId provider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return libregraph.Permission{}, err + } + + statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &resourceId}}) + if errCode := errorcode.FromStat(statResponse, err); errCode != nil { + s.logger.Warn().Err(errCode).Interface("stat.res", statResponse).Msg("stat failed") + return libregraph.Permission{}, *errCode + } + + resourceInfo := statResponse.GetInfo() + condition := unifiedrole.UnifiedRoleConditionGrantee + if IsSpaceRoot(resourceInfo.GetId()) { + condition = unifiedrole.UnifiedRoleConditionOwner + } + + unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: invite.LibreGraphPermissionsActions}} + for _, roleID := range invite.GetRoles() { + role, err := unifiedrole.NewUnifiedRoleFromID(roleID, s.resharingEnabled) + if err != nil { + s.logger.Debug().Err(err).Interface("role", invite.GetRoles()[0]).Msg("unable to convert requested role") + return libregraph.Permission{}, err + } + + allowedResourceActions := unifiedrole.GetAllowedResourceActions(role, condition) + if len(allowedResourceActions) == 0 { + return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource") + } + + unifiedRolePermissions = append(unifiedRolePermissions, conversions.ToPointerSlice(role.GetRolePermissions())...) + } + + driveRecipient := invite.GetRecipients()[0] + + objectID := driveRecipient.GetObjectId() + cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(unifiedRolePermissions) + + createShareRequest := &collaboration.CreateShareRequest{ + ResourceInfo: resourceInfo, + Grant: &collaboration.ShareGrant{ + Permissions: &collaboration.SharePermissions{ + Permissions: cs3ResourcePermissions, + }, + }, + } + + permission := &libregraph.Permission{} + if role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*cs3ResourcePermissions, condition, s.resharingEnabled); role != nil { + permission.Roles = []string{role.GetId()} + } + + if len(permission.GetRoles()) == 0 { + permission.LibreGraphPermissionsActions = unifiedrole.CS3ResourcePermissionsToLibregraphActions(*cs3ResourcePermissions) + } + + switch driveRecipient.GetLibreGraphRecipientType() { + case "group": + group, err := s.identityCache.GetGroup(ctx, objectID) + if err != nil { + s.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup") + return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + } + createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ + Type: storageprovider.GranteeType_GRANTEE_TYPE_GROUP, + Id: &storageprovider.Grantee_GroupId{GroupId: &grouppb.GroupId{ + OpaqueId: group.GetId(), + }}, + } + permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ + Group: &libregraph.Identity{ + DisplayName: group.GetDisplayName(), + Id: conversions.ToPointer(group.GetId()), + }, + } + default: + user, err := s.identityCache.GetUser(ctx, objectID) + if err != nil { + s.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup") + return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + } + + createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ + Type: storageprovider.GranteeType_GRANTEE_TYPE_USER, + Id: &storageprovider.Grantee_UserId{UserId: &userpb.UserId{ + OpaqueId: user.GetId(), + }}, + } + permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ + User: &libregraph.Identity{ + DisplayName: user.GetDisplayName(), + Id: conversions.ToPointer(user.GetId()), + }, + } + } + + if invite.ExpirationDateTime != nil { + createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime) + } + + createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest) + if errCode := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); errCode != nil { + s.logger.Debug().Err(err).Msg("share creation failed") + return libregraph.Permission{}, *errCode + } + + if id := createShareResponse.GetShare().GetId().GetOpaqueId(); id != "" { + permission.Id = conversions.ToPointer(id) + } else if IsSpaceRoot(resourceInfo.GetId()) { + // permissions on a space root are not handled by a share manager so + // they don't get a share-id + permission.SetId(identitySetToSpacePermissionID(permission.GetGrantedToV2())) + } + + if expiration := createShareResponse.GetShare().GetExpiration(); expiration != nil { + permission.SetExpirationDateTime(utils.TSToTime(expiration)) + } + + return *permission, nil +} + +// 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 { + logger log.Logger + driveItemPermissionsService DriveItemPermissionsProvider +} + +// NewDriveItemPermissionsApi creates a new DriveItemPermissionsApi +func NewDriveItemPermissionsApi(driveItemPermissionService DriveItemPermissionsProvider, logger log.Logger) (DriveItemPermissionsApi, error) { + return DriveItemPermissionsApi{ + logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()}, + driveItemPermissionsService: driveItemPermissionService, + }, nil +} + +func (api DriveItemPermissionsApi) Invite(w http.ResponseWriter, r *http.Request) { + _, itemID, err := GetDriveAndItemIDParam(r, &api.logger) + if err != nil { + msg := "invalid driveID or itemID" + 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.Invite(ctx, itemID, *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 new file mode 100644 index 000000000..9491189b8 --- /dev/null +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -0,0 +1,289 @@ +package svc_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "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" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "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/go-chi/chi/v5" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/graph/pkg/identity" + svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" + "github.com/stretchr/testify/mock" + "github.com/tidwall/gjson" +) + +var _ = Describe("DriveItemPermissionsService", func() { + var ( + driveItemPermissionsService svc.DriveItemPermissionsService + gatewayClient *cs3mocks.GatewayAPIClient + gatewaySelector *mocks.Selectable[gateway.GatewayAPIClient] + currentUser = &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: "user", + }, + } + ) + + BeforeEach(func() { + logger := log.NewLogger() + gatewayClient = cs3mocks.NewGatewayAPIClient(GinkgoT()) + + gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT()) + gatewaySelector.On("Next").Return(gatewayClient, nil) + + cache := identity.NewIdentityCache(identity.IdentityCacheWithGatewaySelector(gatewaySelector)) + + service, err := svc.NewDriveItemPermissionsService(logger, gatewaySelector, cache, false) + Expect(err).ToNot(HaveOccurred()) + driveItemPermissionsService = service + }) + + Describe("Invite", func() { + var ( + createShareResponse *collaboration.CreateShareResponse + driveItemInvite libregraph.DriveItemInvite + driveItemId provider.ResourceId + statResponse *provider.StatResponse + getUserResponse *userpb.GetUserResponse + getGroupResponse *grouppb.GetGroupResponse + ) + + BeforeEach(func() { + driveItemId = provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + } + ctx := revactx.ContextSetUser(context.Background(), currentUser) + + statResponse = &provider.StatResponse{ + Status: status.NewOK(ctx), + } + gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil) + + getUserResponse = &userpb.GetUserResponse{ + Status: status.NewOK(ctx), + User: &userpb.User{ + Id: &userpb.UserId{OpaqueId: "1"}, + DisplayName: "Cem Kaner", + }, + } + + getGroupResponse = &grouppb.GetGroupResponse{ + Status: status.NewOK(ctx), + Group: &grouppb.Group{ + Id: &grouppb.GroupId{OpaqueId: "2"}, + GroupName: "Florida Institute of Technology", + DisplayName: "Florida Institute of Technology", + }, + } + + createShareResponse = &collaboration.CreateShareResponse{ + Status: status.NewOK(ctx), + } + }) + + It("creates user shares as expected (happy path)", func() { + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, 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.Invite(context.Background(), driveItemId, 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("creates group shares as expected (happy path)", func() { + gatewayClient.On("GetGroup", mock.Anything, mock.Anything).Return(getGroupResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("2"), LibreGraphRecipientType: libregraph.PtrString("group")}, + } + 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.Invite(context.Background(), driveItemId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + Expect(permission.GetId()).To(Equal("123")) + Expect(permission.GetExpirationDateTime().Equal(*driveItemInvite.ExpirationDateTime)).To(BeTrue()) + Expect(permission.GrantedToV2.Group.GetDisplayName()).To(Equal(getGroupResponse.Group.DisplayName)) + Expect(permission.GrantedToV2.Group.GetId()).To(Equal("2")) + }) + + It("with roles (happy path)", func() { + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, + } + driveItemInvite.Roles = []string{unifiedrole.NewViewerUnifiedRole(true).GetId()} + + permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + + Expect(permission.GetRoles()).To(HaveLen(1)) + Expect(permission.GetRoles()[0]).To(Equal(unifiedrole.NewViewerUnifiedRole(true).GetId())) + }) + + It("fails with wrong role", func() { + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, + } + driveItemInvite.Roles = []string{unifiedrole.NewManagerUnifiedRole().GetId()} + permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite) + + Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource"))) + Expect(permission).To(BeZero()) + }) + + It("with actions (happy path)", func() { + gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) + gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) + driveItemInvite.Recipients = []libregraph.DriveRecipient{ + {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, + } + driveItemInvite.Roles = nil + driveItemInvite.LibreGraphPermissionsActions = []string{unifiedrole.DriveItemContentRead} + + permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite) + Expect(err).ToNot(HaveOccurred()) + + Expect(permission).NotTo(BeZero()) + Expect(permission.GetRoles()).To(HaveLen(0)) + Expect(permission.GetLibreGraphPermissionsActions()).To(HaveLen(1)) + Expect(permission.GetLibreGraphPermissionsActions()[0]).To(Equal(unifiedrole.DriveItemContentRead)) + }) + }) +}) + +var _ = Describe("DriveItemPermissionsApiApi", func() { + var ( + mockProvider *mocks.DriveItemPermissionsProvider + httpAPI svc.DriveItemPermissionsApi + rCTX *chi.Context + invite libregraph.DriveItemInvite + ) + + BeforeEach(func() { + logger := log.NewLogger() + + mockProvider = mocks.NewDriveItemPermissionsProvider(GinkgoT()) + api, err := svc.NewDriveItemPermissionsApi(mockProvider, logger) + Expect(err).ToNot(HaveOccurred()) + + httpAPI = api + + rCTX = chi.NewRouteContext() + rCTX.URLParams.Add("driveID", "1$2") + + invite = libregraph.DriveItemInvite{ + Recipients: []libregraph.DriveRecipient{ + { + ObjectId: libregraph.PtrString("1"), + LibreGraphRecipientType: libregraph.PtrString("user")}, + }, + Roles: []string{unifiedrole.NewViewerUnifiedRole(true).GetId()}, + } + }) + + checkDriveIDAndItemIDValidation := func(handler http.HandlerFunc) { + rCTX.URLParams.Add("itemID", "3$4!5") + + responseRecorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/", nil). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + handler(responseRecorder, request) + + Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity)) + + jsonData := gjson.Get(responseRecorder.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal("invalid driveID or itemID")) + } + + Describe("Invite", func() { + It("validates the driveID and itemID url param", func() { + checkDriveIDAndItemIDValidation(httpAPI.Invite) + }) + + It("return an error when the Invite provider errors", func() { + rCTX.URLParams.Add("itemID", "1$2!3") + 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), + ) + + onInvite := mockProvider.On("Invite", mock.Anything, mock.Anything, mock.Anything) + + onInvite.Return(func(ctx context.Context, resourceID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + return libregraph.Permission{}, errors.New("any") + }).Once() + + httpAPI.Invite(responseRecorder, request) + + Expect(responseRecorder.Code).To(Equal(http.StatusInternalServerError)) + }) + + It("call the Invite provider with the correct arguments", func() { + rCTX.URLParams.Add("itemID", "1$2!3") + responseRecorder := httptest.NewRecorder() + inviteJson, err := json.Marshal(invite) + Expect(err).ToNot(HaveOccurred()) + + onInvite := mockProvider.On("Invite", mock.Anything, mock.Anything, mock.Anything) + onInvite.Return(func(ctx context.Context, resourceID storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) { + Expect(storagespace.FormatResourceID(resourceID)).To(Equal("1$2!3")) + return libregraph.Permission{}, nil + }).Once() + + request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + httpAPI.Invite(responseRecorder, request) + + Expect(responseRecorder.Code).To(Equal(http.StatusOK)) + }) + }) +}) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 7066f3341..ce91964bd 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -15,7 +15,6 @@ import ( "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" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -452,156 +451,6 @@ func (g Graph) ListPermissions(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, collectionOfPermissions) } -// Invite invites a user to a storage drive (space). -func (g Graph) Invite(w http.ResponseWriter, r *http.Request) { - gatewayClient, ok := g.GetGatewayClient(w, r) - if !ok { - return - } - - _, itemID, err := GetDriveAndItemIDParam(r, g.logger) - if err != nil { - errorcode.RenderError(w, r, err) - return - } - - driveItemInvite := &libregraph.DriveItemInvite{} - if err := StrictJSONUnmarshal(r.Body, driveItemInvite); err != nil { - g.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 { - g.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - - statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}}) - if errCode := errorcode.FromStat(statResponse, err); errCode != nil { - g.logger.Warn().Err(errCode).Interface("stat.res", statResponse).Msg("stat failed") - errCode.Render(w, r) - return - } - - condition := unifiedrole.UnifiedRoleConditionGrantee - if IsSpaceRoot(statResponse.GetInfo().GetId()) { - condition = unifiedrole.UnifiedRoleConditionOwner - } - - unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: driveItemInvite.LibreGraphPermissionsActions}} - for _, roleID := range driveItemInvite.GetRoles() { - role, err := unifiedrole.NewUnifiedRoleFromID(roleID, g.config.FilesSharing.EnableResharing) - if err != nil { - g.logger.Debug().Err(err).Interface("role", driveItemInvite.GetRoles()[0]).Msg("unable to convert requested role") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - return - } - - allowedResourceActions := unifiedrole.GetAllowedResourceActions(role, condition) - if len(allowedResourceActions) == 0 { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "role not applicable to this resource") - return - } - - unifiedRolePermissions = append(unifiedRolePermissions, conversions.ToPointerSlice(role.GetRolePermissions())...) - } - - driveRecipient := driveItemInvite.GetRecipients()[0] - - objectID := driveRecipient.GetObjectId() - cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(unifiedRolePermissions) - - createShareRequest := &collaboration.CreateShareRequest{ - ResourceInfo: statResponse.GetInfo(), - Grant: &collaboration.ShareGrant{ - Permissions: &collaboration.SharePermissions{ - Permissions: cs3ResourcePermissions, - }, - }, - } - - permission := &libregraph.Permission{} - if role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*cs3ResourcePermissions, condition, g.config.FilesSharing.EnableResharing); role != nil { - permission.Roles = []string{role.GetId()} - } - - if len(permission.GetRoles()) == 0 { - permission.LibreGraphPermissionsActions = unifiedrole.CS3ResourcePermissionsToLibregraphActions(*cs3ResourcePermissions) - } - - switch driveRecipient.GetLibreGraphRecipientType() { - case "group": - group, err := g.identityCache.GetGroup(ctx, objectID) - if err != nil { - g.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup") - errorcode.GeneralException.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ - Type: storageprovider.GranteeType_GRANTEE_TYPE_GROUP, - Id: &storageprovider.Grantee_GroupId{GroupId: &grouppb.GroupId{ - OpaqueId: group.GetId(), - }}, - } - permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ - Group: &libregraph.Identity{ - DisplayName: group.GetDisplayName(), - Id: conversions.ToPointer(group.GetId()), - }, - } - default: - user, err := g.identityCache.GetUser(ctx, objectID) - if err != nil { - g.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup") - errorcode.GeneralException.Render(w, r, http.StatusBadRequest, err.Error()) - return - } - - createShareRequest.GetGrant().Grantee = &storageprovider.Grantee{ - Type: storageprovider.GranteeType_GRANTEE_TYPE_USER, - Id: &storageprovider.Grantee_UserId{UserId: &userpb.UserId{ - OpaqueId: user.GetId(), - }}, - } - permission.GrantedToV2 = &libregraph.SharePointIdentitySet{ - User: &libregraph.Identity{ - DisplayName: user.GetDisplayName(), - Id: conversions.ToPointer(user.GetId()), - }, - } - } - - if driveItemInvite.ExpirationDateTime != nil { - createShareRequest.GetGrant().Expiration = utils.TimeToTS(*driveItemInvite.ExpirationDateTime) - } - - createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest) - if errCode := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); errCode != nil { - g.logger.Debug().Err(err).Msg("share creation failed") - errCode.Render(w, r) - return - } - - if id := createShareResponse.GetShare().GetId().GetOpaqueId(); id != "" { - permission.Id = conversions.ToPointer(id) - } else if IsSpaceRoot(statResponse.GetInfo().GetId()) { - // permissions on a space root are not handled by a share manager, so - // they don't get a share-id - permission.SetId(identitySetToSpacePermissionID(permission.GetGrantedToV2())) - } - - if expiration := createShareResponse.GetShare().GetExpiration(); expiration != nil { - permission.SetExpirationDateTime(utils.TSToTime(expiration)) - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, &ListResponse{Value: []interface{}{permission}}) -} - // UpdatePermission updates a Permission of a Drive item func (g Graph) UpdatePermission(w http.ResponseWriter, r *http.Request) { _, itemID, err := GetDriveAndItemIDParam(r, g.logger) diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 8f087a85c..339a2f257 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -11,7 +11,6 @@ import ( "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" @@ -22,7 +21,6 @@ import ( . "github.com/onsi/gomega" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/stretchr/testify/mock" - "github.com/tidwall/gjson" "google.golang.org/grpc" roleconversions "github.com/cs3org/reva/v2/pkg/conversions" @@ -30,15 +28,12 @@ import ( "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" "github.com/owncloud/ocis/v2/services/graph/pkg/linktype" - "github.com/cs3org/reva/v2/pkg/storagespace" - revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/utils" cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" - "github.com/owncloud/ocis/v2/ocis-pkg/conversions" "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" @@ -1074,381 +1069,6 @@ var _ = Describe("Driveitems", func() { }) }) - Describe("Invite", func() { - var ( - driveItemInvite *libregraph.DriveItemInvite - statMock *mock.Call - statResponse *provider.StatResponse - getUserResponse *userpb.GetUserResponse - getUserMock *mock.Call - getGroupResponse *grouppb.GetGroupResponse - getGroupMock *mock.Call - createShareResponse *collaboration.CreateShareResponse - createShareMock *mock.Call - ) - - BeforeEach(func() { - rctx := chi.NewRouteContext() - rctx.URLParams.Add("driveID", "1$2") - rctx.URLParams.Add("itemID", "1$2!3") - - ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx) - ctx = revactx.ContextSetUser(ctx, currentUser) - - driveItemInvite = &libregraph.DriveItemInvite{ - Recipients: []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")}, - }, - Roles: []string{unifiedrole.NewViewerUnifiedRole(true).GetId()}, - } - - statMock = gatewayClient.On("Stat", mock.Anything, mock.Anything) - statResponse = &provider.StatResponse{ - Status: status.NewOK(ctx), - } - statMock.Return(statResponse, nil) - - getUserMock = gatewayClient.On("GetUser", mock.Anything, mock.Anything) - getUserResponse = &userpb.GetUserResponse{ - Status: status.NewOK(ctx), - User: &userpb.User{ - Id: &userpb.UserId{OpaqueId: "1"}, - DisplayName: "Cem Kaner", - }, - } - getUserMock.Return(getUserResponse, nil) - - getGroupMock = gatewayClient.On("GetGroup", mock.Anything, mock.Anything) - getGroupResponse = &grouppb.GetGroupResponse{ - Status: status.NewOK(ctx), - Group: &grouppb.Group{ - Id: &grouppb.GroupId{OpaqueId: "2"}, - GroupName: "Florida Institute of Technology", - }, - } - getGroupMock.Return(getGroupResponse, nil) - - createShareMock = gatewayClient.On("CreateShare", mock.Anything, mock.Anything) - createShareResponse = &collaboration.CreateShareResponse{ - Status: status.NewOK(ctx), - } - createShareMock.Return(createShareResponse, nil) - }) - - toJSONReader := func(v any) *strings.Reader { - driveItemInviteBytes, err := json.Marshal(v) - Expect(err).ToNot(HaveOccurred()) - - return strings.NewReader(string(driveItemInviteBytes)) - } - - It("creates user shares as expected (happy path)", func() { - 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), - } - - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - jsonData := gjson.Get(rr.Body.String(), "value") - - Expect(rr.Code).To(Equal(http.StatusOK)) - Expect(jsonData.Get("#").Num).To(Equal(float64(1))) - - Expect(jsonData.Get("0.id").Str).To(Equal("123")) - Expect(jsonData.Get("0.expirationDateTime").Str).To(Equal(driveItemInvite.ExpirationDateTime.Format(time.RFC3339Nano))) - Expect(jsonData.Get("#.grantedToV2.user.displayName").Array()[0].Str).To(Equal(getUserResponse.User.DisplayName)) - Expect(jsonData.Get("#.grantedToV2.user.id").Array()[0].Str).To(Equal("1")) - }) - - It("creates group shares as expected (happy path)", func() { - driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("2"), LibreGraphRecipientType: libregraph.PtrString("group")}, - } - driveItemInvite.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(time.Hour)) - createShareResponse.Share = &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: "123"}, - Expiration: utils.TimeToTS(*driveItemInvite.ExpirationDateTime), - } - - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - jsonData := gjson.Get(rr.Body.String(), "value") - - Expect(rr.Code).To(Equal(http.StatusOK)) - Expect(jsonData.Get("#").Num).To(Equal(float64(1))) - Expect(jsonData.Get("0.id").Str).To(Equal("123")) - Expect(jsonData.Get("0.expirationDateTime").Str).To(Equal(driveItemInvite.ExpirationDateTime.Format(time.RFC3339Nano))) - Expect(jsonData.Get("#.grantedToV2.group.displayName").Array()[0].Str).To(Equal(getGroupResponse.Group.GroupName)) - Expect(jsonData.Get("#.grantedToV2.group.id").Array()[0].Str).To(Equal("2")) - }) - - It("with roles (happy path)", func() { - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - jsonData := gjson.Get(rr.Body.String(), "value") - - Expect(rr.Code).To(Equal(http.StatusOK)) - - Expect(jsonData.Get(`0.@libre\.graph\.permissions\.actions`).Exists()).To(BeFalse()) - Expect(jsonData.Get("0.roles.#").Num).To(Equal(float64(1))) - Expect(jsonData.Get("0.roles.0").String()).To(Equal(unifiedrole.NewViewerUnifiedRole(true).GetId())) - }) - - It("fails with wrong role", func() { - driveItemInvite.Roles = []string{unifiedrole.NewManagerUnifiedRole().GetId()} - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(http.StatusBadRequest)) - }) - - It("with actions (happy path)", func() { - driveItemInvite.Roles = nil - driveItemInvite.LibreGraphPermissionsActions = []string{unifiedrole.DriveItemContentRead} - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - jsonData := gjson.Get(rr.Body.String(), "value") - - Expect(rr.Code).To(Equal(http.StatusOK)) - - Expect(jsonData.Get("0.roles").Exists()).To(BeFalse()) - Expect(jsonData.Get(`0.@libre\.graph\.permissions\.actions.#`).Num).To(Equal(float64(1))) - Expect(jsonData.Get(`0.@libre\.graph\.permissions\.actions.0`).String()).To(Equal(unifiedrole.DriveItemContentRead)) - }) - - It("fails if the request body is empty", func() { - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", nil). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(http.StatusBadRequest)) - }) - - DescribeTable("request validations", - func(body func() *strings.Reader, code int) { - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", body()). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(code)) - }, - Entry("fails on unknown fields", func() *strings.Reader { - return strings.NewReader(`{"unknown":"field"}`) - }, http.StatusBadRequest), - ) - - DescribeTable("GetGroup", - func(prep func(), code int) { - driveItemInvite.Recipients = []libregraph.DriveRecipient{ - {ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("group")}, - } - - prep() - - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(code)) - getGroupMock.Parent.AssertNumberOfCalls(GinkgoT(), "GetGroup", 1) - }, - Entry("fails if not ok", func() { - getGroupResponse.Status = status.NewNotFound(context.Background(), "") - }, http.StatusBadRequest), - Entry("fails if errors", func() { - getGroupMock.Return(nil, errors.New("error")) - }, http.StatusBadRequest), - ) - - DescribeTable("GetUser", - func(prep func(), code int) { - prep() - - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(code)) - getUserMock.Parent.AssertNumberOfCalls(GinkgoT(), "GetUser", 1) - }, - Entry("fails if not ok", func() { - getUserResponse.Status = status.NewInvalid(context.Background(), "") - }, http.StatusBadRequest), - Entry("fails if errors", func() { - getUserMock.Return(nil, errors.New("error")) - }, http.StatusBadRequest), - ) - - DescribeTable("CreateShare", - func(prep func(), code int) { - prep() - - svc.Invite( - rr, - httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(code)) - createShareMock.Parent.AssertNumberOfCalls(GinkgoT(), "CreateShare", 1) - }, - Entry("fails if not ok", func() { - createShareResponse.Status = status.NewNotFound(context.Background(), "") - }, http.StatusNotFound), - Entry("fails if errors", func() { - createShareMock.Return(nil, errors.New("error")) - }, http.StatusInternalServerError), - ) - }) - - Describe("ListPermissions", func() { - var ( - statMock *mock.Call - statResponse *provider.StatResponse - listSharesMock *mock.Call - listSharesResponse *collaboration.ListSharesResponse - listPublicSharesMock *mock.Call - listPublicSharesResponse *link.ListPublicSharesResponse - rctx *chi.Context - ) - - toResourceID := func(in string) *provider.ResourceId { - out, err := storagespace.ParseID(in) - Expect(err).To(BeNil()) - - return &out - } - - BeforeEach(func() { - rctx = chi.NewRouteContext() - ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx) - ctx = revactx.ContextSetUser(ctx, currentUser) - - statMock = gatewayClient.On("Stat", mock.Anything, mock.Anything) - statResponse = &provider.StatResponse{ - Status: status.NewOK(ctx), - Info: &provider.ResourceInfo{ - Id: toResourceID("1$2!3"), - PermissionSet: unifiedrole.PermissionsToCS3ResourcePermissions( - conversions.ToPointerSlice(unifiedrole.NewViewerUnifiedRole(true).GetRolePermissions()), - ), - Owner: &userpb.UserId{}, - }, - } - statMock.Return(statResponse, nil) - - listSharesMock = gatewayClient.On("ListShares", mock.Anything, mock.Anything) - listSharesResponse = &collaboration.ListSharesResponse{ - Status: status.NewOK(ctx), - Shares: []*collaboration.Share{{ - Id: &collaboration.ShareId{OpaqueId: "123"}, - ResourceId: toResourceID("1$2!3"), - Grantee: &provider.Grantee{}, - Permissions: &collaboration.SharePermissions{ - Permissions: unifiedrole.PermissionsToCS3ResourcePermissions( - conversions.ToPointerSlice(unifiedrole.NewViewerUnifiedRole(true).GetRolePermissions()), - ), - }, - }}, - } - listSharesMock.Return(listSharesResponse, nil) - - listPublicSharesMock = gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything) - listPublicSharesResponse = &link.ListPublicSharesResponse{ - Status: status.NewOK(ctx), - } - listPublicSharesMock.Return(listPublicSharesResponse, nil) - }) - - It("lists permissions", func() { - rctx.URLParams.Add("driveID", "1$2") - rctx.URLParams.Add("itemID", "1$2!3") - - svc.ListPermissions( - rr, - httptest.NewRequest(http.MethodGet, "/", nil). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(http.StatusOK)) - - actions := gjson.Get(rr.Body.String(), `@libre\.graph\.permissions\.actions\.allowedValues`) - Expect(actions.Get("#").Num).To(Equal(float64(7))) - - roles := gjson.Get(rr.Body.String(), `@libre\.graph\.permissions\.roles\.allowedValues`) - Expect(roles.Get("#").Num).To(Equal(float64(1))) - Expect(roles.Get("0.id").Str).To(Equal("b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5")) - Expect(roles.Get("0.rolePermissions").Exists()).To(BeFalse()) - - value := gjson.Get(rr.Body.String(), "value") - Expect(value.Get("#").Num).To(Equal(float64(1))) - Expect(value.Get("0.id").Str).To(Equal("123")) - }) - It("lists permissions on a storage space", func() { - rctx.URLParams.Add("driveID", "1$2") - rctx.URLParams.Add("itemID", "1$2!2") - statResponse.Info.Id.OpaqueId = "2" - gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil) - - getUserMock := gatewayClient.On("GetUser", mock.Anything, mock.Anything) - getUserMockResponse := &userpb.GetUserResponse{ - Status: status.NewOK(ctx), - User: &userpb.User{ - Id: &userpb.UserId{OpaqueId: "userid"}, - DisplayName: "Test User", - }, - } - getUserMock.Return(getUserMockResponse, nil) - - svc.ListPermissions( - rr, - httptest.NewRequest(http.MethodGet, "/", nil). - WithContext(ctx), - ) - - Expect(rr.Code).To(Equal(http.StatusOK)) - p := libregraph.NewCollectionOfPermissions() - err := json.Unmarshal(rr.Body.Bytes(), p) - Expect(err).To(BeNil()) - permissions := p.GetValue() - Expect(len(permissions)).To(Equal(1)) - Expect(permissions[0].GetId()).ToNot(Equal("")) - }) - - }) - Describe("GetRootDriveChildren", func() { It("handles ListStorageSpaces not found", func() { gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{ diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 25c19b2dc..b12f231b7 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -112,7 +112,6 @@ type Service interface { CreateLink(w http.ResponseWriter, r *http.Request) SetLinkPassword(writer http.ResponseWriter, request *http.Request) - Invite(w http.ResponseWriter, r *http.Request) ListPermissions(w http.ResponseWriter, r *http.Request) UpdatePermission(w http.ResponseWriter, r *http.Request) DeletePermission(w http.ResponseWriter, r *http.Request) @@ -214,6 +213,16 @@ func NewService(opts ...Option) (Graph, error) { return svc, err } + driveItemPermissionsService, err := NewDriveItemPermissionsService(options.Logger, options.GatewaySelector, identityCache, options.Config.FilesSharing.EnableResharing) + if err != nil { + return svc, err + } + + driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger) + if err != nil { + return svc, err + } + m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Use(middleware.StripSlashes) @@ -231,7 +240,7 @@ func NewService(opts ...Option) (Graph, error) { r.Post("/root/children", drivesDriveItemApi.CreateDriveItem) r.Route("/items/{itemID}", func(r chi.Router) { r.Delete("/", drivesDriveItemApi.DeleteDriveItem) - r.Post("/invite", svc.Invite) + r.Post("/invite", driveItemPermissionsApi.Invite) r.Route("/permissions", func(r chi.Router) { r.Get("/", svc.ListPermissions) r.Route("/{permissionID}", func(r chi.Router) { diff --git a/tests/acceptance/features/apiSharingNg/shareInvitations.feature b/tests/acceptance/features/apiSharingNg/shareInvitations.feature index 7e08cf5f9..b6307f87b 100644 --- a/tests/acceptance/features/apiSharingNg/shareInvitations.feature +++ b/tests/acceptance/features/apiSharingNg/shareInvitations.feature @@ -889,7 +889,7 @@ Feature: Send a sharing invitations "code": { "type": "string", "enum": [ - "generalException" + "invalidRequest" ] }, "message": { @@ -940,7 +940,7 @@ Feature: Send a sharing invitations "properties": { "code": { "type": "string", - "pattern": "generalException" + "pattern": "invalidRequest" }, "message": { "type": "string", @@ -1113,7 +1113,7 @@ Feature: Send a sharing invitations "code": { "type": "string", "enum": [ - "generalException" + "invalidRequest" ] }, "message": { @@ -1224,7 +1224,7 @@ Feature: Send a sharing invitations "properties": { "code": { "type": "string", - "enum": ["generalException"] + "enum": ["invalidRequest"] }, "message": { "type": "string", @@ -2375,14 +2375,14 @@ Feature: Send a sharing invitations | sharee | grp1 | | shareType | group | | permissionsAction | | - Then the HTTP status code should be "400" + Then the HTTP status code should be "400" When user "Alice" sends the following share invitation using the Graph API: | resource | FolderToShare | | space | Personal | | sharee | Brian | | shareType | user | | permissionsAction | | - Then the HTTP status code should be "400" + Then the HTTP status code should be "400" Examples: | permissions-action | | permissions/create |