diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 88ba09c089..c48bbee037 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -96,6 +96,7 @@ type Service interface { GetDrives(w http.ResponseWriter, r *http.Request) GetSingleDrive(w http.ResponseWriter, r *http.Request) GetAllDrives(w http.ResponseWriter, r *http.Request) + GetSharedByMe(w http.ResponseWriter, r *http.Request) CreateDrive(w http.ResponseWriter, r *http.Request) UpdateDrive(w http.ResponseWriter, r *http.Request) DeleteDrive(w http.ResponseWriter, r *http.Request) @@ -199,6 +200,9 @@ func NewService(opts ...Option) (Graph, error) { m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Use(middleware.StripSlashes) + r.Route("/v1beta1", func(r chi.Router) { + r.Get("/me/drives/sharedByMe", svc.GetSharedByMe) + }) r.Route("/v1.0", func(r chi.Router) { r.Route("/extensions/org.libregraph", func(r chi.Router) { r.Get("/tags", svc.GetTags) @@ -212,7 +216,9 @@ func NewService(opts ...Option) (Graph, error) { r.Route("/me", func(r chi.Router) { r.Get("/", svc.GetMe) r.Get("/drive", svc.GetUserDrive) - r.Get("/drives", svc.GetDrives) + r.Route("/drives", func(r chi.Router) { + r.Get("/", svc.GetDrives) + }) r.Get("/drive/root/children", svc.GetRootDriveChildren) r.Post("/changePassword", svc.ChangeOwnPassword) }) diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go new file mode 100644 index 0000000000..ad1644c376 --- /dev/null +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -0,0 +1,149 @@ +package svc + +import ( + "context" + "net/http" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + "github.com/cs3org/reva/v2/pkg/share" + "github.com/cs3org/reva/v2/pkg/storagespace" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" +) + +type driveItemsByResourceID map[string]libregraph.DriveItem + +// GetSharedByMe implements the Service interface (/me/drives/sharedByMe endpoint) +func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) { + g.logger.Debug().Msg("Calling GetRootDriveChildren") + ctx := r.Context() + + driveItems := make(driveItemsByResourceID) + var err error + driveItems, err = g.listUserShares(ctx, driveItems) + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + _, err = g.listPublicShares(ctx, driveItems) + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + res := make([]libregraph.DriveItem, 0, len(driveItems)) + for _, v := range driveItems { + res = append(res, v) + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, &ListResponse{Value: res}) +} + +func (g Graph) listUserShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Error().Err(err).Msg("could not select next gateway client") + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + + filters := []*collaboration.Filter{ + share.UserGranteeFilter(), + share.GroupGranteeFilter(), + } + lsUserSharesRequest := collaboration.ListSharesRequest{ + Filters: filters, + } + + lsUserSharesResponse, err := gatewayClient.ListShares(ctx, &lsUserSharesRequest) + if err != nil { + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + if statusCode := lsUserSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK { + return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsUserSharesResponse.Status.Message) + } + driveItems, err = g.cs3UserSharesToDriveItems(ctx, lsUserSharesResponse.Shares, driveItems) + if err != nil { + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + return driveItems, nil +} + +func (g Graph) listPublicShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { + + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Error().Err(err).Msg("could not select next gateway client") + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + + filters := []*link.ListPublicSharesRequest_Filter{} + + req := link.ListPublicSharesRequest{ + Filters: filters, + } + + lsPublicSharesResponse, err := gatewayClient.ListPublicShares(ctx, &req) + if err != nil { + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + if statusCode := lsPublicSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK { + return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsPublicSharesResponse.Status.Message) + } + driveItems, err = g.cs3PublicSharesToDriveItems(ctx, lsPublicSharesResponse.Share, driveItems) + if err != nil { + return driveItems, errorcode.New(errorcode.GeneralException, err.Error()) + } + return driveItems, nil + +} + +func (g Graph) cs3UserSharesToDriveItems(ctx context.Context, shares []*collaboration.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { + for _, s := range shares { + g.logger.Debug().Interface("CS3 UserShare", s).Msg("Got Share") + resIDStr := storagespace.FormatResourceID(*s.ResourceId) + item, ok := driveItems[resIDStr] + if !ok { + item = libregraph.DriveItem{ + Id: libregraph.PtrString(resIDStr), + } + } + driveItems[resIDStr] = item + } + + return driveItems, nil +} + +func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) { + for _, s := range shares { + g.logger.Debug().Interface("CS3 PublicShare", s).Msg("Got Share") + resIDStr := storagespace.FormatResourceID(*s.ResourceId) + item, ok := driveItems[resIDStr] + if !ok { + item = libregraph.DriveItem{ + Id: libregraph.PtrString(resIDStr), + } + } + driveItems[resIDStr] = item + } + + return driveItems, nil +} + +func cs3StatusToErrCode(code rpc.Code) (errcode errorcode.ErrorCode) { + switch code { + case rpc.Code_CODE_UNAUTHENTICATED: + errcode = errorcode.Unauthenticated + case rpc.Code_CODE_PERMISSION_DENIED: + errcode = errorcode.AccessDenied + case rpc.Code_CODE_NOT_FOUND: + errcode = errorcode.ItemNotFound + default: + errcode = errorcode.GeneralException + } + return errcode +} diff --git a/services/graph/pkg/service/v0/sharedbyme_test.go b/services/graph/pkg/service/v0/sharedbyme_test.go new file mode 100644 index 0000000000..75b844a582 --- /dev/null +++ b/services/graph/pkg/service/v0/sharedbyme_test.go @@ -0,0 +1,304 @@ +package svc_test + +import ( + "context" + "encoding/json" + "errors" + "io" + "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" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "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/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + libregraph "github.com/owncloud/libre-graph-api-go" + "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" + "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" + identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks" + service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" +) + +var _ = Describe("Driveitems", func() { + var ( + svc service.Service + ctx context.Context + cfg *config.Config + gatewayClient *cs3mocks.GatewayAPIClient + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + eventsPublisher mocks.Publisher + identityBackend *identitymocks.Backend + + rr *httptest.ResponseRecorder + + newGroup *libregraph.Group + ) + + 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{} + gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return( + &link.ListPublicSharesResponse{ + Status: status.NewOK(ctx), + Share: []*link.PublicShare{}, + }, + nil, + ) + gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient]( + "GatewaySelector", + "com.owncloud.api.gateway", + func(cc *grpc.ClientConn) gateway.GatewayAPIClient { + return gatewayClient + }, + ) + + identityBackend = &identitymocks.Backend{} + newGroup = libregraph.NewGroup() + newGroup.SetMembersodataBind([]string{"/users/user1"}) + newGroup.SetId("group1") + + rr = httptest.NewRecorder() + ctx = context.Background() + + cfg = defaults.FullDefaultConfig() + cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests + cfg.TokenManager.JWTSecret = "loremipsum" + cfg.Commons = &shared.Commons{} + cfg.GRPCClientTLS = &shared.GRPCClientTLS{} + + svc, _ = service.NewService( + service.Config(cfg), + service.WithGatewaySelector(gatewaySelector), + service.EventsPublisher(&eventsPublisher), + service.WithIdentityBackend(identityBackend), + ) + }) + + Describe("GetSharedByMe", func() { + expiration := time.Now() + userShare := collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "share-id", + }, + ResourceId: &provider.ResourceId{ + StorageId: "storageid", + SpaceId: "spaceid", + OpaqueId: "opaqueid", + }, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + OpaqueId: "user-id", + }, + }, + }, + } + groupShare := collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "share-id", + }, + ResourceId: &provider.ResourceId{ + StorageId: "storageid", + SpaceId: "spaceid", + OpaqueId: "opaqueid", + }, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_GROUP, + Id: &provider.Grantee_GroupId{ + GroupId: &grouppb.GroupId{ + OpaqueId: "group-id", + }, + }, + }, + } + userShareWithExpiration := collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: "expire-share-id", + }, + ResourceId: &provider.ResourceId{ + StorageId: "storageid", + SpaceId: "spaceid", + OpaqueId: "expire-opaqueid", + }, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + OpaqueId: "user-id", + }, + }, + }, + Expiration: utils.TimeToTS(expiration), + } + + It("handles a failing ListShares", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(nil, errors.New("some error")) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusInternalServerError)) + }) + + It("handles ListShares returning an error status", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{Status: status.NewInternal(ctx, "error listing shares")}, + nil, + ) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusInternalServerError)) + }) + + It("succeeds, when no shares are returned", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{ + Status: status.NewOK(ctx), + Shares: []*collaboration.Share{}, + }, + nil, + ) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(0)) + }) + + It("returns a proper driveItem, when a single user share is returned", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{ + Status: status.NewOK(ctx), + Shares: []*collaboration.Share{ + &userShare, + }, + }, + nil, + ) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + + di := res.Value[0] + Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShare.GetResourceId()))) + }) + + It("returns a proper driveItem, when a single group share is returned", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{ + Status: status.NewOK(ctx), + Shares: []*collaboration.Share{ + &groupShare, + }, + }, + nil, + ) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + + di := res.Value[0] + Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId()))) + }) + + It("returns a single driveItem, when a mulitple shares for the same resource are returned", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{ + Status: status.NewOK(ctx), + Shares: []*collaboration.Share{ + &groupShare, + &userShare, + }, + }, + nil, + ) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + + di := res.Value[0] + Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId()))) + }) + + It("return a driveItem with the expiration date set, for expiring shares", func() { + gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return( + &collaboration.ListSharesResponse{ + Status: status.NewOK(ctx), + Shares: []*collaboration.Share{ + &userShareWithExpiration, + }, + }, + nil, + ) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil) + svc.GetSharedByMe(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + + di := res.Value[0] + Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShareWithExpiration.GetResourceId()))) + }) + }) +})