refactor(graph): move "ListPermissions" to drive item permissions service

This commit is contained in:
Ralf Haferkamp
2024-03-26 18:03:15 +01:00
committed by Ralf Haferkamp
parent ae53a97cc0
commit 2a5b406963
5 changed files with 293 additions and 111 deletions

View File

@@ -82,6 +82,63 @@ func (_c *DriveItemPermissionsProvider_Invite_Call) RunAndReturn(run func(contex
return _c
}
// ListPermissions provides a mock function with given fields: ctx, itemID
func (_m *DriveItemPermissionsProvider) ListPermissions(ctx context.Context, itemID providerv1beta1.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _m.Called(ctx, itemID)
if len(ret) == 0 {
panic("no return value specified for ListPermissions")
}
var r0 libregraph.CollectionOfPermissionsWithAllowedValues
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return rf(ctx, itemID)
}
if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = rf(ctx, itemID)
} else {
r0 = ret.Get(0).(libregraph.CollectionOfPermissionsWithAllowedValues)
}
if rf, ok := ret.Get(1).(func(context.Context, providerv1beta1.ResourceId) error); ok {
r1 = rf(ctx, itemID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_ListPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPermissions'
type DriveItemPermissionsProvider_ListPermissions_Call struct {
*mock.Call
}
// ListPermissions is a helper method to define mock.On call
// - ctx context.Context
// - itemID providerv1beta1.ResourceId
func (_e *DriveItemPermissionsProvider_Expecter) ListPermissions(ctx interface{}, itemID interface{}) *DriveItemPermissionsProvider_ListPermissions_Call {
return &DriveItemPermissionsProvider_ListPermissions_Call{Call: _e.mock.On("ListPermissions", ctx, itemID)}
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Run(run func(ctx context.Context, itemID providerv1beta1.ResourceId)) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(providerv1beta1.ResourceId))
})
return _c
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Return(_a0 libregraph.CollectionOfPermissionsWithAllowedValues, _a1 error) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) RunAndReturn(run func(context.Context, providerv1beta1.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Return(run)
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)

View File

@@ -8,8 +8,11 @@ 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"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/render"
@@ -26,6 +29,7 @@ import (
type DriveItemPermissionsProvider interface {
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)
ListPermissions(ctx context.Context, itemID storageprovider.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
}
// DriveItemPermissionsService contains the production business logic for everything that relates to permissions on drive items.
@@ -188,6 +192,77 @@ func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveI
return s.Invite(ctx, *rootResourceID, invite)
}
// ListPermissions lists the permissions of a driveItem
func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID storageprovider.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return collectionOfPermissions, err
}
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
if errCode := errorcode.FromStat(statResponse, err); errCode != nil {
s.logger.Warn().Err(errCode).Interface("stat.res", statResponse).Msg("stat failed")
return collectionOfPermissions, err
}
condition := unifiedrole.UnifiedRoleConditionGrantee
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
condition = unifiedrole.UnifiedRoleConditionOwner
}
permissionSet := *statResponse.GetInfo().GetPermissionSet()
allowedActions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissionSet)
collectionOfPermissions = libregraph.CollectionOfPermissionsWithAllowedValues{
LibreGraphPermissionsActionsAllowedValues: allowedActions,
LibreGraphPermissionsRolesAllowedValues: conversions.ToValueSlice(
unifiedrole.GetApplicableRoleDefinitionsForActions(
allowedActions,
condition,
s.config.FilesSharing.EnableResharing,
false,
),
),
}
for i, definition := range collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues {
// the openapi spec defines that the rolePermissions should not be part of the response
definition.RolePermissions = nil
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues[i] = definition
}
driveItems := make(driveItemsByResourceID)
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
permissions, err := s.getSpaceRootPermissions(ctx, statResponse.GetInfo().GetSpace().GetId())
if err != nil {
return collectionOfPermissions, err
}
collectionOfPermissions.Value = permissions
} else {
// "normal" driveItem, populate user permissions via share providers
driveItems, err = s.listUserShares(ctx, []*collaboration.Filter{
share.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
}
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
for _, driveItem := range driveItems {
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
return collectionOfPermissions, 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 {
@@ -269,3 +344,24 @@ func (api DriveItemPermissionsApi) SpaceRootInvite(w http.ResponseWriter, r *htt
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: []interface{}{permission}})
}
func (api DriveItemPermissionsApi) ListPermissions(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.RenderError(w, r, err)
return
}
ctx := r.Context()
permissions, err := api.driveItemPermissionsService.ListPermissions(ctx, itemID)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, permissions)
}

View File

@@ -13,8 +13,10 @@ 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"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
roleconversions "github.com/cs3org/reva/v2/pkg/conversions"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/storagespace"
@@ -40,11 +42,14 @@ var _ = Describe("DriveItemPermissionsService", func() {
driveItemPermissionsService svc.DriveItemPermissionsService
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector *mocks.Selectable[gateway.GatewayAPIClient]
getUserResponse *userpb.GetUserResponse
currentUser = &userpb.User{
Id: &userpb.UserId{
OpaqueId: "user",
},
}
statResponse *provider.StatResponse
ctx context.Context
)
BeforeEach(func() {
@@ -60,6 +65,18 @@ var _ = Describe("DriveItemPermissionsService", func() {
service, err := svc.NewDriveItemPermissionsService(logger, gatewaySelector, cache, cfg)
Expect(err).ToNot(HaveOccurred())
driveItemPermissionsService = service
ctx = revactx.ContextSetUser(context.Background(), currentUser)
statResponse = &provider.StatResponse{
Status: status.NewOK(ctx),
}
getUserResponse = &userpb.GetUserResponse{
Status: status.NewOK(ctx),
User: &userpb.User{
Id: &userpb.UserId{OpaqueId: "1"},
DisplayName: "Cem Kaner",
},
}
})
Describe("Invite", func() {
@@ -67,8 +84,6 @@ var _ = Describe("DriveItemPermissionsService", func() {
createShareResponse *collaboration.CreateShareResponse
driveItemInvite libregraph.DriveItemInvite
driveItemId provider.ResourceId
statResponse *provider.StatResponse
getUserResponse *userpb.GetUserResponse
getGroupResponse *grouppb.GetGroupResponse
)
@@ -78,21 +93,9 @@ var _ = Describe("DriveItemPermissionsService", func() {
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{
@@ -204,7 +207,6 @@ var _ = Describe("DriveItemPermissionsService", func() {
createShareResponse *collaboration.CreateShareResponse
driveItemInvite libregraph.DriveItemInvite
driveId provider.ResourceId
statResponse *provider.StatResponse
getUserResponse *userpb.GetUserResponse
)
@@ -213,11 +215,6 @@ var _ = Describe("DriveItemPermissionsService", func() {
StorageId: "1",
SpaceId: "2",
}
ctx := revactx.ContextSetUser(context.Background(), currentUser)
statResponse = &provider.StatResponse{
Status: status.NewOK(ctx),
}
listSpacesResponse = &provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
@@ -288,6 +285,92 @@ var _ = Describe("DriveItemPermissionsService", func() {
Expect(permission).To(BeZero())
})
})
Describe("ListPermissions", func() {
var (
itemID provider.ResourceId
listSharesResponse *collaboration.ListSharesResponse
listPublicSharesResponse *link.ListPublicSharesResponse
)
BeforeEach(func() {
itemID = provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
}
listSharesResponse = &collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{},
}
listPublicSharesResponse = &link.ListPublicSharesResponse{
Status: status.NewOK(ctx),
}
})
It("populates allowedValues for files that are not shared", func() {
statResponse.Info = &provider.ResourceInfo{
Id: &itemID,
PermissionSet: roleconversions.NewViewerRole(false).CS3ResourcePermissions(),
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID)
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
})
It("returns one permission per share", func() {
statResponse.Info = &provider.ResourceInfo{
Id: &itemID,
PermissionSet: roleconversions.NewEditorRole(false).CS3ResourcePermissions(),
}
listSharesResponse.Shares = []*collaboration.Share{
{
Id: &collaboration.ShareId{OpaqueId: "1"},
Permissions: &collaboration.SharePermissions{
Permissions: roleconversions.NewViewerRole(false).CS3ResourcePermissions(),
},
ResourceId: &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: "user-id",
},
},
},
},
}
listPublicSharesResponse.Share = []*link.PublicShare{
{
Id: &link.PublicShareId{
OpaqueId: "public-share-id",
},
Token: "public-share-token",
ResourceId: &provider.ResourceId{
StorageId: "storageid",
SpaceId: "spaceid",
OpaqueId: "public-share-opaqueid",
},
Permissions: &link.PublicSharePermissions{Permissions: roleconversions.NewViewerRole(false).CS3ResourcePermissions()},
},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID)
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
Expect(len(permissions.Value)).To(Equal(2))
})
})
})
var _ = Describe("DriveItemPermissionsApi", func() {
@@ -405,7 +488,7 @@ var _ = Describe("DriveItemPermissionsApi", func() {
Expect(responseRecorder.Code).To(Equal(http.StatusOK))
})
It("call the Invite provider with the correct arguments", func() {
It("fails with an empty driveid", func() {
rCTX.URLParams.Add("driveID", "")
responseRecorder := httptest.NewRecorder()
inviteJson, err := json.Marshal(invite)
@@ -420,4 +503,39 @@ var _ = Describe("DriveItemPermissionsApi", func() {
Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity))
})
})
Describe("ListPermissions", func() {
It("calls the ListPermissions 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())
mockProvider.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).
Return(func(ctx context.Context, itemid storageprovider.ResourceId) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
Expect(storagespace.FormatResourceID(itemid)).To(Equal("1$2!3"))
return libregraph.CollectionOfPermissionsWithAllowedValues{}, nil
}).Once()
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)).
WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
)
httpAPI.ListPermissions(responseRecorder, request)
Expect(responseRecorder.Code).To(Equal(http.StatusOK))
})
It("fails with an empty itemid", func() {
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.ListPermissions(responseRecorder, request)
Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest))
})
})
})

View File

@@ -27,14 +27,10 @@ import (
"golang.org/x/crypto/sha3"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/share"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"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/unifiedrole"
@@ -367,90 +363,6 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &ListResponse{Value: files})
}
// ListPermissions lists the permissions of a driveItem
func (g Graph) ListPermissions(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
}
ctx := r.Context()
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
}
permissionSet := *statResponse.GetInfo().GetPermissionSet()
allowedActions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissionSet)
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{
LibreGraphPermissionsActionsAllowedValues: allowedActions,
LibreGraphPermissionsRolesAllowedValues: conversions.ToValueSlice(
unifiedrole.GetApplicableRoleDefinitionsForActions(
allowedActions,
condition,
g.config.FilesSharing.EnableResharing,
false,
),
),
}
for i, definition := range collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues {
// the openapi spec defines that the rolePermissions should not be part of the response
definition.RolePermissions = nil
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues[i] = definition
}
driveItems := make(driveItemsByResourceID)
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
permissions, err := g.getSpaceRootPermissions(ctx, statResponse.GetInfo().GetSpace().GetId())
if err != nil {
errorcode.RenderError(w, r, err)
return
}
collectionOfPermissions.Value = permissions
} else {
// "normal" driveItem, populate user permissions via share providers
driveItems, err = g.listUserShares(ctx, []*collaboration.Filter{
share.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
}
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = g.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
for _, driveItem := range driveItems {
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
render.Status(r, http.StatusOK)
render.JSON(w, r, collectionOfPermissions)
}
// UpdatePermission updates a Permission of a Drive item
func (g Graph) UpdatePermission(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, g.logger)

View File

@@ -112,7 +112,6 @@ type Service interface {
CreateLink(w http.ResponseWriter, r *http.Request)
SetLinkPassword(writer http.ResponseWriter, request *http.Request)
ListPermissions(w http.ResponseWriter, r *http.Request)
UpdatePermission(w http.ResponseWriter, r *http.Request)
DeletePermission(w http.ResponseWriter, r *http.Request)
@@ -247,7 +246,7 @@ func NewService(opts ...Option) (Graph, error) {
r.Delete("/", drivesDriveItemApi.DeleteDriveItem)
r.Post("/invite", driveItemPermissionsApi.Invite)
r.Route("/permissions", func(r chi.Router) {
r.Get("/", svc.ListPermissions)
r.Get("/", driveItemPermissionsApi.ListPermissions)
r.Route("/{permissionID}", func(r chi.Router) {
r.Delete("/", svc.DeletePermission)
r.Patch("/", svc.UpdatePermission)