diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 1c0bfb5c7..c655c2d8f 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -19,6 +19,7 @@ import ( collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" "golang.org/x/crypto/sha3" @@ -252,23 +253,9 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) { return } - driveID, err := parseIDParam(r, "driveID") + _, itemID, err := g.extractDriveAndDriveItem(r) if err != nil { - g.logger.Debug().Err(err).Msg("could not parse driveID") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid driveID") - return - } - - itemID, err := parseIDParam(r, "itemID") - if err != nil { - g.logger.Debug().Err(err).Msg("could not parse itemID") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid itemID") - return - } - - if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() { - g.logger.Debug().Interface("driveID", driveID).Interface("itemID", itemID).Msg("driveID and itemID do not match") - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "driveID and itemID do not match") + errorcode.RenderError(w, r, err) return } @@ -447,6 +434,104 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &ListResponse{Value: value}) } +// DeletePermission removes a Permission from a Drive item +func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return + } + + _, itemID, err := g.extractDriveAndDriveItem(r) + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID")) + + if err != nil { + g.logger.Debug().Err(err).Msg("could not parse driveID") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid driveID") + return + } + + ctx := r.Context() + getShareResp, err := gatewayClient.GetShare(ctx, + &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: permissionID, + }, + }, + }, + }) + switch { + case err != nil: + fallthrough + case getShareResp.Status.GetCode() != cs3rpc.Code_CODE_OK: + g.logger.Debug().Err(err).Interface("permissionID", permissionID).Interface("GetShare", getShareResp).Msg("GetShare failed") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return + } + + sharedResourceId := getShareResp.GetShare().GetResourceId() + // The resourceID of the shared resource need to matched the item ID from the Request Path + // otherwise this is an invalid Request. + if sharedResourceId.GetStorageId() != itemID.GetStorageId() || + sharedResourceId.GetSpaceId() != itemID.GetSpaceId() || + sharedResourceId.GetOpaqueId() != itemID.GetOpaqueId() { + g.logger.Debug().Msg("resourceID of shared does not match itemID") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "permissionID and itemID do not match") + return + } + + removeShareResp, err := gatewayClient.RemoveShare(ctx, + &collaboration.RemoveShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: permissionID, + }, + }, + }, + }) + switch { + case err != nil: + fallthrough + case removeShareResp.Status.GetCode() != cs3rpc.Code_CODE_OK: + g.logger.Debug().Err(err).Interface("permissionID", permissionID).Interface("GetShare", getShareResp).Msg("GetShare failed") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return + } + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) + + return +} + +func (g Graph) extractDriveAndDriveItem(r *http.Request) (driveID storageprovider.ResourceId, itemID storageprovider.ResourceId, err error) { + driveID, err = parseIDParam(r, "driveID") + if err != nil { + g.logger.Debug().Err(err).Msg("could not parse driveID") + return driveID, itemID, errorcode.New(errorcode.InvalidRequest, "invalid driveID") + } + + itemID, err = parseIDParam(r, "itemID") + if err != nil { + g.logger.Debug().Err(err).Msg("could not parse itemID") + return driveID, itemID, errorcode.New(errorcode.InvalidRequest, "invalid itemID") + } + + if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() { + g.logger.Debug().Interface("driveID", driveID).Interface("itemID", itemID).Msg("driveID and itemID do not match") + return driveID, itemID, errorcode.New(errorcode.InvalidRequest, "driveID and itemID do not match") + } + return driveID, itemID, nil +} + func (g Graph) getDriveItem(ctx context.Context, ref storageprovider.Reference) (*libregraph.DriveItem, error) { gatewayClient, err := g.gatewaySelector.Next() if err != nil { diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 4487737c3..b5441ed61 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -100,6 +100,137 @@ var _ = Describe("Driveitems", func() { ) }) + Describe("DeletePermission", func() { + It("validates the driveID", func() { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "") + + ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + + svc.DeletePermission( + rr, + httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx), + ) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("validates the itemID", func() { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "f0042750-23c5-441c-9f2c-ff7c53e5bd2a$cd621428-dfbe-44c1-9393-65bf0dd440a6!cd621428-dfbe-44c1-9393-65bf0dd440a6") + rctx.URLParams.Add("itemID", "") + + ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + + svc.DeletePermission( + rr, + httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx), + ) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("checks if the itemID and driveID is compatible to each other", func() { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "1$2!3") + rctx.URLParams.Add("itemID", "4$5!6") + + ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + + svc.DeletePermission( + rr, + httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx), + ) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("deletes a permission as expected", func() { + getShareMock := gatewayClient.On("GetShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.GetShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + getShareMockResponse := &collaboration.GetShareResponse{ + Status: status.NewOK(ctx), + Share: &collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "permissionid", + }, + ResourceId: &provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + }, + }, + } + getShareMock.Return(getShareMockResponse, nil) + + rmShareMock := gatewayClient.On("RemoveShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.RemoveShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + rmShareMockResponse := &collaboration.RemoveShareResponse{ + Status: status.NewOK(ctx), + } + rmShareMock.Return(rmShareMockResponse, nil) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "1$2") + rctx.URLParams.Add("itemID", "1$2!3") + rctx.URLParams.Add("permissionID", "permissionid") + + ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + + svc.DeletePermission( + rr, + httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx), + ) + + Expect(rr.Code).To(Equal(http.StatusNoContent)) + }) + + It("fails to delete permission when the item id does not match the shared resource's id", func() { + getShareMock := gatewayClient.On("GetShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.GetShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + getShareMockResponse := &collaboration.GetShareResponse{ + Status: status.NewOK(ctx), + Share: &collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "permissionid", + }, + ResourceId: &provider.ResourceId{ + StorageId: "3", + SpaceId: "4", + OpaqueId: "5", + }, + }, + } + getShareMock.Return(getShareMockResponse, nil) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "1$2") + rctx.URLParams.Add("itemID", "1$2!3") + rctx.URLParams.Add("permissionID", "permissionid") + + ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + + svc.DeletePermission( + rr, + httptest.NewRequest(http.MethodPost, "/", nil).WithContext(ctx), + ) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + }) + Describe("Invite", func() { var ( itemID string diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 7f9f0d392..7f866c4f7 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -109,6 +109,7 @@ type Service interface { GetDriveItemChildren(w http.ResponseWriter, r *http.Request) Invite(w http.ResponseWriter, r *http.Request) + DeletePermission(w http.ResponseWriter, r *http.Request) GetTags(w http.ResponseWriter, r *http.Request) AssignTags(w http.ResponseWriter, r *http.Request) @@ -198,7 +199,10 @@ func NewService(opts ...Option) (Graph, error) { r.Route("/v1beta1", func(r chi.Router) { r.Get("/me/drive/sharedByMe", svc.GetSharedByMe) r.Get("/me/drive/sharedWithMe", svc.ListSharedWithMe) - r.Post("/drives/{driveID}/items/{itemID}/invite", svc.Invite) + r.Route("/drives/{driveID}/items/{itemID}", func(r chi.Router) { + r.Post("/invite", svc.Invite) + r.Delete("/permissions/{permissionID}", svc.DeletePermission) + }) r.Route("/roleManagement/permissions/roleDefinitions", func(r chi.Router) { r.Get("/", svc.GetRoleDefinitions) r.Get("/{roleID}", svc.GetRoleDefinition)