From 7b40fdcbf708525a5a73d70c8bc95b01f313e99c Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 30 Nov 2023 17:44:41 +0100 Subject: [PATCH 1/3] graph sharing: Add help to convert CS3 share to libregraph.Permission --- services/graph/pkg/service/v0/sharedbyme.go | 118 +++++++++++--------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index 085b44c4a4f..036ca21b8a3 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -124,63 +124,77 @@ func (g Graph) cs3UserSharesToDriveItems(ctx context.Context, shares []*collabor } item = *itemptr } - perm := libregraph.Permission{} - perm.SetRoles([]string{}) - perm.SetId(s.Id.OpaqueId) - grantedTo := libregraph.SharePointIdentitySet{} - var li libregraph.Identity - switch s.Grantee.Type { - case storageprovider.GranteeType_GRANTEE_TYPE_USER: - user, err := g.identityCache.GetUser(ctx, s.Grantee.GetUserId().GetOpaqueId()) - switch { - case errors.Is(err, identity.ErrNotFound): - g.logger.Warn().Str("userid", s.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id") - // User does not seem to exist anymore, don't add a permission for this - continue - case err != nil: - return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) - default: - li.SetDisplayName(user.GetDisplayName()) - li.SetId(user.GetId()) - grantedTo.SetUser(li) - } - case storageprovider.GranteeType_GRANTEE_TYPE_GROUP: - group, err := g.identityCache.GetGroup(ctx, s.Grantee.GetGroupId().GetOpaqueId()) - switch { - case errors.Is(err, identity.ErrNotFound): - g.logger.Warn().Str("groupid", s.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id") - // Group not seem to exist anymore, don't add a permission for this - continue - case err != nil: - return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) - default: - li.SetDisplayName(group.GetDisplayName()) - li.SetId(group.GetId()) - grantedTo.SetGroup(li) - } + perm, err := g.cs3UserShareToPermission(ctx, s) + + var errcode errorcode.Error + switch { + case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound: + // The Grantee couldn't be found (user/group does not exist anymore) + continue + case err != nil: + return driveItems, err } + item.Permissions = append(item.Permissions, *perm) + driveItems[resIDStr] = item + } + return driveItems, nil +} - // set expiration date - if s.GetExpiration() != nil { - perm.SetExpirationDateTime(cs3TimestampToTime(s.GetExpiration())) +func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share) (*libregraph.Permission, error) { + perm := libregraph.Permission{} + perm.SetRoles([]string{}) + perm.SetId(share.Id.OpaqueId) + grantedTo := libregraph.SharePointIdentitySet{} + var li libregraph.Identity + switch share.Grantee.Type { + case storageprovider.GranteeType_GRANTEE_TYPE_USER: + user, err := g.identityCache.GetUser(ctx, share.Grantee.GetUserId().GetOpaqueId()) + switch { + case errors.Is(err, identity.ErrNotFound): + g.logger.Warn().Str("userid", share.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id") + // User does not seem to exist anymore, don't add a permission for this + return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist") + case err != nil: + return nil, errorcode.New(errorcode.GeneralException, err.Error()) + default: + li.SetDisplayName(user.GetDisplayName()) + li.SetId(user.GetId()) + grantedTo.SetUser(li) } - role := unifiedrole.CS3ResourcePermissionsToUnifiedRole( - *s.GetPermissions().GetPermissions(), - unifiedrole.UnifiedRoleConditionGrantee, - g.config.FilesSharing.EnableResharing, - ) - if role != nil { - perm.SetRoles([]string{role.GetId()}) - } else { - actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*s.GetPermissions().GetPermissions()) - perm.SetLibreGraphPermissionsActions(actions) - perm.SetRoles(nil) + case storageprovider.GranteeType_GRANTEE_TYPE_GROUP: + group, err := g.identityCache.GetGroup(ctx, share.Grantee.GetGroupId().GetOpaqueId()) + switch { + case errors.Is(err, identity.ErrNotFound): + g.logger.Warn().Str("groupid", share.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id") + // Group not seem to exist anymore, don't add a permission for this + return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist") + case err != nil: + return nil, errorcode.New(errorcode.GeneralException, err.Error()) + default: + li.SetDisplayName(group.GetDisplayName()) + li.SetId(group.GetId()) + grantedTo.SetGroup(li) } - perm.SetGrantedToV2(grantedTo) - item.Permissions = append(item.Permissions, perm) - driveItems[resIDStr] = item } - return driveItems, nil + + // set expiration date + if share.GetExpiration() != nil { + perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration())) + } + role := unifiedrole.CS3ResourcePermissionsToUnifiedRole( + *share.GetPermissions().GetPermissions(), + unifiedrole.UnifiedRoleConditionGrantee, + g.config.FilesSharing.EnableResharing, + ) + if role != nil { + perm.SetRoles([]string{role.GetId()}) + } else { + actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*share.GetPermissions().GetPermissions()) + perm.SetLibreGraphPermissionsActions(actions) + perm.SetRoles(nil) + } + perm.SetGrantedToV2(grantedTo) + return &perm, nil } func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { From dbf23a9738bfebe7263f500f66feed727a156d42 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 30 Nov 2023 17:51:45 +0100 Subject: [PATCH 2/3] graph sharing: Properly dereference errorcode.Error before returning Otherwise errorcode.RenderError() will not render the correct HTTP Status --- services/graph/pkg/service/v0/driveitems.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 38e960204ea..d76bcd05199 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -629,7 +629,7 @@ func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) { // Check if the id is refering to a User Share sharedResourceId, err := g.getUserPermissionResourceID(ctx, permissionID) - var errcode *errorcode.Error + var errcode errorcode.Error if err != nil && errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound { // there is no user share with that ID, so lets check if it is referring to a public link isUserPermission = false @@ -684,7 +684,7 @@ func (g Graph) getUserPermissionResourceID(ctx context.Context, permissionID str }, }) if errCode := errorcode.FromCS3Status(getShareResp.GetStatus(), err); errCode != nil { - return nil, errCode + return nil, *errCode } return getShareResp.Share.GetResourceId(), nil } @@ -708,7 +708,7 @@ func (g Graph) removeUserShare(ctx context.Context, permissionID string) error { }) if errCode := errorcode.FromCS3Status(removeShareResp.GetStatus(), err); errCode != nil { - return errCode + return *errCode } // We need to return an untyped nil here otherwise the error==nil check won't work return nil @@ -733,7 +733,7 @@ func (g Graph) getLinkPermissionResourceID(ctx context.Context, permissionID str }, ) if errCode := errorcode.FromCS3Status(getPublicShareResp.GetStatus(), err); errCode != nil { - return nil, errCode + return nil, *errCode } return getPublicShareResp.Share.GetResourceId(), nil } @@ -756,7 +756,7 @@ func (g Graph) removePublicShare(ctx context.Context, permissionID string) error }, }) if errcode := errorcode.FromCS3Status(removePublicShareResp.GetStatus(), err); errcode != nil { - return errcode + return *errcode } // We need to return an untyped nil here otherwise the error==nil check won't work return nil From 269ce605dd61f1ba88b4c3fd96fcaae1c0cea01a Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 30 Nov 2023 17:53:18 +0100 Subject: [PATCH 3/3] graph sharing: Implement UpdatePermissions This is an initial implementation of PATCH support on drives/{driveid}/items/{itemid}/permissions/{id}. It focusses on updating user shares for now. It's possible to update the expirationDate, roles and/or libregraphResourceActions. Updating the permissions of a space root or a public link share is currently not implemeted. --- services/graph/pkg/service/v0/driveitems.go | 185 +++++++++++- .../graph/pkg/service/v0/driveitems_test.go | 268 +++++++++++++++++- services/graph/pkg/service/v0/service.go | 12 + services/graph/pkg/service/v0/sharedbyme.go | 2 +- services/graph/pkg/unifiedrole/unifiedrole.go | 9 + services/graph/pkg/validate/libregraph.go | 97 ++++--- 6 files changed, 532 insertions(+), 41 deletions(-) diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index d76bcd05199..83406696f51 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -27,6 +27,7 @@ import ( libregraph "github.com/owncloud/libre-graph-api-go" "golang.org/x/crypto/sha3" "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/cs3org/reva/v2/pkg/publicshare" "github.com/cs3org/reva/v2/pkg/share" @@ -609,6 +610,65 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &ListResponse{Value: value}) } +// UpdatePermission updates a Permission of a Drive item +func (g Graph) UpdatePermission(w http.ResponseWriter, r *http.Request) { + _, itemID, err := g.GetDriveAndItemIDParam(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 permissionID") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID") + return + } + + permission := &libregraph.Permission{} + if err := StrictJSONUnmarshal(r.Body, permission); 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, permission); 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 + } + + oldPermission, sharedResourceId, err := g.getPermissionByID(ctx, permissionID) + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + // The resourceID of the shared resource need to match the item ID from the Request Path + // otherwise this is an invalid Request. + if !utils.ResourceIDEqual(sharedResourceId, &itemID) { + 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 + } + + // We don't implement updating link permissions yet + if _, ok := oldPermission.GetLinkOk(); ok { + errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, "not implemented") + return + } + // This is a user share + updatedPermission, err := g.updateUserShare(ctx, permissionID, oldPermission, permission) + if err != nil { + errorcode.RenderError(w, r, err) + } + render.Status(r, http.StatusOK) + render.JSON(w, r, &updatedPermission) + return +} + // DeletePermission removes a Permission from a Drive item func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) { _, itemID, err := g.GetDriveAndItemIDParam(r) @@ -666,7 +726,43 @@ func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) { return } +func (g Graph) getPermissionByID(ctx context.Context, permissionID string) (*libregraph.Permission, *storageprovider.ResourceId, error) { + share, err := g.getCS3UserShareByID(ctx, permissionID) + if err == nil { + permission, err := g.cs3UserShareToPermission(ctx, share) + if err != nil { + return nil, nil, err + } + return permission, share.GetResourceId(), nil + } + + var errcode errorcode.Error + if errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound { + // there is no user share with that id, check if this is a public link + publicShare, err := g.getCS3PublicShareByID(ctx, permissionID) + if err != nil { + return nil, nil, err + } + permission, err := g.libreGraphPermissionFromCS3PublicShare(publicShare) + if err != nil { + return nil, nil, err + } + return permission, publicShare.GetResourceId(), nil + } + + return nil, nil, err + +} + func (g Graph) getUserPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) { + share, err := g.getCS3UserShareByID(ctx, permissionID) + if err != nil { + return nil, err + } + return share.GetResourceId(), nil +} + +func (g Graph) getCS3UserShareByID(ctx context.Context, permissionID string) (*collaboration.Share, error) { gatewayClient, err := g.gatewaySelector.Next() if err != nil { g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed") @@ -686,7 +782,84 @@ func (g Graph) getUserPermissionResourceID(ctx context.Context, permissionID str if errCode := errorcode.FromCS3Status(getShareResp.GetStatus(), err); errCode != nil { return nil, *errCode } - return getShareResp.Share.GetResourceId(), nil + return getShareResp.GetShare(), nil +} + +func (g Graph) updateUserShare(ctx context.Context, permissionID string, oldPermission, newPermission *libregraph.Permission) (*libregraph.Permission, error) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed") + return nil, err + } + + cs3UpdateShareReq := &collaboration.UpdateShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: permissionID, + }, + }, + }, + Share: &collaboration.Share{}, + } + fieldmask := []string{} + if expiration, ok := newPermission.GetExpirationDateTimeOk(); ok { + fieldmask = append(fieldmask, "expiration") + if expiration != nil { + cs3UpdateShareReq.Share.Expiration = utils.TimeToTS(*expiration) + } + } + var roles, allowedResourceActions []string + var permissionsUpdated, ok bool + if roles, ok = newPermission.GetRolesOk(); ok && len(roles) > 0 { + for _, roleId := range roles { + role, err := unifiedrole.NewUnifiedRoleFromID(roleId, g.config.FilesSharing.EnableResharing) + if err != nil { + g.logger.Debug().Err(err).Interface("role", role).Msg("unable to convert requested role") + return nil, err + } + + // FIXME: When setting permissions on a space, we need to use UnifiedRoleConditionOwner here + allowedResourceActions = unifiedrole.GetAllowedResourceActions(role, unifiedrole.UnifiedRoleConditionGrantee) + if len(allowedResourceActions) == 0 { + return nil, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource") + } + } + permissionsUpdated = true + } else if allowedResourceActions, ok = newPermission.GetLibreGraphPermissionsActionsOk(); ok && len(allowedResourceActions) > 0 { + permissionsUpdated = true + } + + if permissionsUpdated { + cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions( + []*libregraph.UnifiedRolePermission{ + { + + AllowedResourceActions: allowedResourceActions, + }, + }, + ) + cs3UpdateShareReq.Share.Permissions = &collaboration.SharePermissions{ + Permissions: cs3ResourcePermissions, + } + fieldmask = append(fieldmask, "permissions") + } + + cs3UpdateShareReq.UpdateMask = &fieldmaskpb.FieldMask{ + Paths: fieldmask, + } + + updateUserShareResp, err := gatewayClient.UpdateShare(ctx, cs3UpdateShareReq) + if errCode := errorcode.FromCS3Status(updateUserShareResp.GetStatus(), err); errCode != nil { + return nil, *errCode + } + + permission, err := g.cs3UserShareToPermission(ctx, updateUserShareResp.GetShare()) + if err != nil { + return nil, err + } + + return permission, nil } func (g Graph) removeUserShare(ctx context.Context, permissionID string) error { @@ -715,6 +888,14 @@ func (g Graph) removeUserShare(ctx context.Context, permissionID string) error { } func (g Graph) getLinkPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) { + share, err := g.getCS3PublicShareByID(ctx, permissionID) + if err != nil { + return nil, err + } + return share.GetResourceId(), nil +} + +func (g Graph) getCS3PublicShareByID(ctx context.Context, permissionID string) (*link.PublicShare, error) { gatewayClient, err := g.gatewaySelector.Next() if err != nil { g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed") @@ -735,7 +916,7 @@ func (g Graph) getLinkPermissionResourceID(ctx context.Context, permissionID str if errCode := errorcode.FromCS3Status(getPublicShareResp.GetStatus(), err); errCode != nil { return nil, *errCode } - return getPublicShareResp.Share.GetResourceId(), nil + return getPublicShareResp.GetShare(), nil } func (g Graph) removePublicShare(ctx context.Context, permissionID string) error { diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index e3eb2cb2562..909d6ed3ff8 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -12,10 +12,12 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/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" + roleconversions "github.com/cs3org/reva/v2/pkg/conversions" "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -70,8 +72,6 @@ var _ = Describe("Driveitems", func() { BeforeEach(func() { eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) - rr = httptest.NewRecorder() - pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway") gatewayClient = &cs3mocks.GatewayAPIClient{} gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient]( @@ -248,6 +248,270 @@ var _ = Describe("Driveitems", func() { }) }) + Describe("UpdatePermission", func() { + var ( + driveItemPermission *libregraph.Permission + getShareMockResponse *collaboration.GetShareResponse + getPublicShareMockResponse *link.GetPublicShareResponse + getUserMockResponse *user.GetUserResponse + updateShareMockResponse *collaboration.UpdateShareResponse + ) + BeforeEach(func() { + rr = httptest.NewRecorder() + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "1$2") + rctx.URLParams.Add("itemID", "1$2!3") + rctx.URLParams.Add("permissionID", "permissionid") + + ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx) + ctx = revactx.ContextSetUser(ctx, currentUser) + + driveItemPermission = &libregraph.Permission{} + + getUserMock := gatewayClient.On("GetUser", mock.Anything, mock.Anything) + getUserMockResponse = &userpb.GetUserResponse{ + Status: status.NewOK(ctx), + User: &userpb.User{ + Id: &userpb.UserId{OpaqueId: "useri"}, + DisplayName: "Test User", + }, + } + getUserMock.Return(getUserMockResponse, nil) + + getShareMock := gatewayClient.On("GetShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.GetShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + share := &collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "permissionid", + }, + ResourceId: &provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + }, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: &user.UserId{ + OpaqueId: "userid", + }, + }, + }, + Permissions: &collaboration.SharePermissions{ + Permissions: roleconversions.NewViewerRole(true).CS3ResourcePermissions(), + }, + } + getShareMockResponse = &collaboration.GetShareResponse{ + Status: status.NewOK(ctx), + Share: share, + } + getShareMock.Return(getShareMockResponse, nil) + + updateShareMockResponse = &collaboration.UpdateShareResponse{ + Status: status.NewOK(ctx), + Share: share, + } + + getPublicShareMock := gatewayClient.On("GetPublicShare", + mock.Anything, + mock.MatchedBy(func(req *link.GetPublicShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + getPublicShareMockResponse = &link.GetPublicShareResponse{ + Status: status.NewOK(ctx), + Share: &link.PublicShare{ + Id: &link.PublicShareId{ + OpaqueId: "permissionid", + }, + ResourceId: &provider.ResourceId{ + StorageId: "1", + SpaceId: "2", + OpaqueId: "3", + }, + Permissions: &link.PublicSharePermissions{ + Permissions: roleconversions.NewViewerRole(true).CS3ResourcePermissions(), + }, + Token: "token", + }, + } + getPublicShareMock.Return(getPublicShareMockResponse, nil) + + }) + It("fails when no share is found", func() { + getShareMockResponse.Share = nil + getShareMockResponse.Status = status.NewNotFound(ctx, "not found") + getPublicShareMockResponse.Share = nil + getPublicShareMockResponse.Status = status.NewNotFound(ctx, "not found") + + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusNotFound)) + }) + // Updating a public link will be implemented later + It("fails when trying to update a link permission", func() { + getShareMockResponse.Share = nil + getShareMockResponse.Status = status.NewNotFound(ctx, "not found") + + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusNotImplemented)) + }) + It("fails updating the id", func() { + driveItemPermission.SetId("permissionid") + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + It("updates the expiration date", func() { + expiration := time.Now().Add(time.Hour) + updateShareMock := gatewayClient.On("UpdateShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool { + if req.GetRef().GetId().GetOpaqueId() == "permissionid" { + return expiration.Equal(utils.TSToTime(req.GetShare().GetExpiration())) + } + return false + }), + ) + updateShareMockResponse.Share.Expiration = utils.TimeToTS(expiration) + updateShareMock.Return(updateShareMockResponse, nil) + + driveItemPermission.SetExpirationDateTime(expiration) + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := libregraph.Permission{} + + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + Expect(res.GetExpirationDateTime().Equal(expiration)).To(BeTrue()) + }) + It("deletes the expiration date", func() { + updateShareMock := gatewayClient.On("UpdateShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool { + if req.GetRef().GetId().GetOpaqueId() == "permissionid" { + return true + } + return false + }), + ) + updateShareMock.Return(updateShareMockResponse, nil) + + driveItemPermission.SetExpirationDateTimeNil() + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := libregraph.Permission{} + + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + _, ok := res.GetExpirationDateTimeOk() + Expect(ok).To(BeFalse()) + }) + It("updates the share permissions with changing the role", func() { + updateShareMock := gatewayClient.On("UpdateShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + updateShareMock.Return(updateShareMockResponse, nil) + + driveItemPermission.SetRoles([]string{unifiedrole.NewViewerUnifiedRole(true).GetId()}) + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := libregraph.Permission{} + + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + _, ok := res.GetRolesOk() + Expect(ok).To(BeTrue()) + }) + It("updates the share permissions when changing the resource permission actions", func() { + updateShareMock := gatewayClient.On("UpdateShare", + mock.Anything, + mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool { + return req.GetRef().GetId().GetOpaqueId() == "permissionid" + }), + ) + updateShareMockResponse.Share.Permissions = &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + GetPath: true, + }, + } + + updateShareMock.Return(updateShareMockResponse, nil) + + driveItemPermission.SetLibreGraphPermissionsActions([]string{unifiedrole.DriveItemPathRead}) + body, err := driveItemPermission.MarshalJSON() + Expect(err).To(BeNil()) + svc.UpdatePermission( + rr, + httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))). + WithContext(ctx), + ) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := libregraph.Permission{} + + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + _, ok := res.GetRolesOk() + Expect(ok).To(BeFalse()) + _, ok = res.GetLibreGraphPermissionsActionsOk() + Expect(ok).To(BeTrue()) + }) + }) + Describe("Invite", func() { var ( driveItemInvite *libregraph.DriveItemInvite diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index b552a2b0b76..fd0679bd588 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -113,6 +113,7 @@ type Service interface { 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) CreateUploadSession(w http.ResponseWriter, r *http.Request) @@ -210,6 +211,17 @@ func NewService(opts ...Option) (Graph, error) { r.Get("/sharedWithMe", svc.ListSharedWithMe) }) }) + r.Route("/drives/{driveID}/items/{itemID}", func(r chi.Router) { + r.Post("/invite", svc.Invite) + r.Route("/permissions", func(r chi.Router) { + r.Get("/", svc.ListPermissions) + r.Route("/{permissionID}", func(r chi.Router) { + r.Delete("/", svc.DeletePermission) + r.Patch("/", svc.UpdatePermission) + }) + }) + r.Post("/createLink", svc.CreateLink) + }) r.Route("/drives", func(r chi.Router) { r.Get("/", svc.GetAllDrives(APIVersion_1_Beta_1)) diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index 036ca21b8a3..0d308b847b9 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -146,7 +146,7 @@ func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboratio perm.SetId(share.Id.OpaqueId) grantedTo := libregraph.SharePointIdentitySet{} var li libregraph.Identity - switch share.Grantee.Type { + switch share.GetGrantee().GetType() { case storageprovider.GranteeType_GRANTEE_TYPE_USER: user, err := g.identityCache.GetUser(ctx, share.Grantee.GetUserId().GetOpaqueId()) switch { diff --git a/services/graph/pkg/unifiedrole/unifiedrole.go b/services/graph/pkg/unifiedrole/unifiedrole.go index bfc3e1ec833..58890f8e81c 100644 --- a/services/graph/pkg/unifiedrole/unifiedrole.go +++ b/services/graph/pkg/unifiedrole/unifiedrole.go @@ -486,3 +486,12 @@ func convert(role *conversions.Role) []string { } return CS3ResourcePermissionsToLibregraphActions(*role.CS3ResourcePermissions()) } + +func GetAllowedResourceActions(role *libregraph.UnifiedRoleDefinition, condition string) []string { + for _, p := range role.GetRolePermissions() { + if p.GetCondition() == condition { + return p.GetAllowedResourceActions() + } + } + return []string{} +} diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go index 0317e2a5a03..3cd523d0785 100644 --- a/services/graph/pkg/validate/libregraph.go +++ b/services/graph/pkg/validate/libregraph.go @@ -12,6 +12,7 @@ import ( // initLibregraph initializes libregraph validation func initLibregraph(v *validator.Validate) { driveItemInvite(v) + permission(v) } // driveItemInvite validates libregraph.DriveItemInvite @@ -27,55 +28,79 @@ func driveItemInvite(v *validator.Validate) { v.RegisterStructValidation(func(sl validator.StructLevel) { driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite) - totalRoles := len(driveItemInvite.Roles) - totalActions := len(driveItemInvite.LibreGraphPermissionsActions) + rolesAndActions(sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false) - switch { - case totalRoles != 0 && totalActions != 0: - fallthrough - case totalRoles == totalActions: - sl.ReportError(driveItemInvite.Roles, "Roles", "Roles", "one_or_another", "") - sl.ReportError(driveItemInvite.LibreGraphPermissionsActions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "") + }, s) +} + +// permission validates libregraph.Permission +func permission(v *validator.Validate) { + s := libregraph.Permission{} + + v.RegisterStructValidationMapRules(map[string]string{ + "Roles": "max=1", + }, s) + v.RegisterStructValidation(func(sl validator.StructLevel) { + permission := sl.Current().Interface().(libregraph.Permission) + + if _, ok := permission.GetIdOk(); ok { + sl.ReportError(permission.Id, "Id", "Id", "readonly", "") } - var availableRoles []string - var availableActions []string - for _, definition := range append( - unifiedrole.GetBuiltinRoleDefinitionList(true), - unifiedrole.GetBuiltinRoleDefinitionList(false)..., - ) { - if slices.Contains(availableRoles, definition.GetId()) { - continue - } + rolesAndActions(sl, permission.Roles, permission.LibreGraphPermissionsActions, true) + }, s) +} - availableRoles = append(availableRoles, definition.GetId()) +func rolesAndActions(sl validator.StructLevel, roles, actions []string, allowEmpty bool) { + totalRoles := len(roles) + totalActions := len(actions) + + switch { + case allowEmpty && totalRoles == 0 && totalActions == 0: + break + case totalRoles != 0 && totalActions != 0: + fallthrough + case totalRoles == totalActions: + sl.ReportError(roles, "Roles", "Roles", "one_or_another", "") + sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "") + } + + var availableRoles []string + var availableActions []string + for _, definition := range append( + unifiedrole.GetBuiltinRoleDefinitionList(true), + unifiedrole.GetBuiltinRoleDefinitionList(false)..., + ) { + if slices.Contains(availableRoles, definition.GetId()) { + continue + } - for _, permission := range definition.GetRolePermissions() { - for _, action := range permission.GetAllowedResourceActions() { - if slices.Contains(availableActions, action) { - continue - } + availableRoles = append(availableRoles, definition.GetId()) - availableActions = append(availableActions, action) + for _, permission := range definition.GetRolePermissions() { + for _, action := range permission.GetAllowedResourceActions() { + if slices.Contains(availableActions, action) { + continue } - } - } - for _, role := range driveItemInvite.Roles { - if slices.Contains(availableRoles, role) { - continue + availableActions = append(availableActions, action) } + } + } - sl.ReportError(driveItemInvite.Roles, "Roles", "Roles", "available_role", "") + for _, role := range roles { + if slices.Contains(availableRoles, role) { + continue } - for _, role := range driveItemInvite.LibreGraphPermissionsActions { - if slices.Contains(availableActions, role) { - continue - } + sl.ReportError(roles, "Roles", "Roles", "available_role", "") + } - sl.ReportError(driveItemInvite.LibreGraphPermissionsActions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "") + for _, role := range actions { + if slices.Contains(availableActions, role) { + continue } - }, s) + sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "") + } }