From c035fc80a9280c6c6de5d8f82f68b6d21f024564 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Sat, 25 Nov 2023 10:27:00 +0100 Subject: [PATCH] feat(api): add new sharing NG create link feature --- services/graph/pkg/service/v0/links.go | 141 ++++++++++++++++++ services/graph/pkg/service/v0/service.go | 2 + services/graph/pkg/service/v0/sharedbyme.go | 25 +--- .../graph/pkg/service/v0/sharedbyme_test.go | 50 ++++--- 4 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 services/graph/pkg/service/v0/links.go diff --git a/services/graph/pkg/service/v0/links.go b/services/graph/pkg/service/v0/links.go new file mode 100644 index 000000000..d86a4e2fc --- /dev/null +++ b/services/graph/pkg/service/v0/links.go @@ -0,0 +1,141 @@ +package svc + +import ( + "context" + "net/http" + "net/url" + "path" + "strconv" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/services/graph/pkg/linktype" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" +) + +func (g Graph) CreateLink(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("calling create link") + driveID, err := parseIDParam(r, "driveID") + if err != nil { + errorcode.RenderError(w, r, err) + return + } + driveItemID, err := parseIDParam(r, "itemID") + if err != nil { + errorcode.RenderError(w, r, err) + return + } + if driveID.StorageId != driveItemID.StorageId || driveID.SpaceId != driveItemID.SpaceId { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist") + return + } + var createLink libregraph.DriveItemCreateLink + if err := StrictJSONUnmarshal(r.Body, &createLink); err != nil { + logger.Error().Err(err).Interface("body", r.Body).Msg("could not create link: invalid body schema definition") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition") + return + } + + createdLink, err := g.createLink(r.Context(), &driveItemID, createLink) + if err != nil { + errorcode.RenderError(w, r, err) + return + } + + perm, err := g.libreGraphPermissionFromCS3PublicShare(createdLink) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(w, r, []libregraph.Permission{*perm}) +} + +func (g Graph) createLink(ctx context.Context, driveItemID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (*link.PublicShare, error) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + g.logger.Error().Err(err).Msg("could not select next gateway client") + return nil, errorcode.New(errorcode.GeneralException, err.Error()) + } + + statResp, err := gatewayClient.Stat( + ctx, + &providerv1beta1.StatRequest{ + Ref: &providerv1beta1.Reference{ + ResourceId: driveItemID, + Path: ".", + }, + }) + if err != nil { + g.logger.Error().Err(err).Msg("transport error, could not stat resource") + return nil, errorcode.New(errorcode.GeneralException, err.Error()) + } + if code := statResp.GetStatus().GetCode(); code != rpc.Code_CODE_OK { + g.logger.Debug().Interface("itemID", driveItemID).Msg(statResp.GetStatus().GetMessage()) + return nil, errorcode.New(cs3StatusToErrCode(code), statResp.GetStatus().GetMessage()) + } + permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(createLink, statResp.GetInfo().GetType()) + if err != nil { + g.logger.Debug().Interface("createLink", createLink).Msg(err.Error()) + return nil, errorcode.New(errorcode.InvalidRequest, "invalid link type") + } + req := link.CreatePublicShareRequest{ + ResourceInfo: statResp.GetInfo(), + Grant: &link.Grant{ + Permissions: &link.PublicSharePermissions{ + Permissions: permissions, + }, + Password: createLink.GetPassword(), + }, + } + // set displayname and password protected as arbitrary metadata + req.ResourceInfo.ArbitraryMetadata = &providerv1beta1.ArbitraryMetadata{ + Metadata: map[string]string{ + "name": createLink.GetDisplayName(), + "quicklink": strconv.FormatBool(createLink.GetLibreGraphQuickLink()), + }, + } + createResp, err := gatewayClient.CreatePublicShare(ctx, &req) + if err != nil { + g.logger.Error().Err(err).Msg("transport error, could not create link") + return nil, errorcode.New(errorcode.GeneralException, err.Error()) + } + if statusCode := createResp.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK { + return nil, errorcode.New(cs3StatusToErrCode(statusCode), createResp.Status.Message) + } + return createResp.GetShare(), nil +} + +func (g Graph) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) { + webURL, err := url.Parse(g.config.Spaces.WebDavBase) + if err != nil { + g.logger.Error(). + Err(err). + Str("url", g.config.Spaces.WebDavBase). + Msg("failed to parse webURL base url") + return nil, err + } + lt, actions := linktype.SharingLinkTypeFromCS3Permissions(createdLink.GetPermissions()) + perm := libregraph.NewPermission() + perm.Id = libregraph.PtrString(createdLink.GetId().GetOpaqueId()) + perm.Link = &libregraph.SharingLink{ + Type: lt, + PreventsDownload: libregraph.PtrBool(false), + LibreGraphDisplayName: libregraph.PtrString(createdLink.GetDisplayName()), + LibreGraphQuickLink: libregraph.PtrBool(createdLink.GetQuicklink()), + } + perm.LibreGraphPermissionsActions = actions + webURL.Path = path.Join(webURL.Path, "s", createdLink.GetToken()) + perm.Link.SetWebUrl(webURL.String()) + + // set expiration date + if createdLink.GetExpiration() != nil { + perm.SetExpirationDateTime(cs3TimestampToTime(createdLink.GetExpiration())) + } + return perm, nil +} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 7f866c4f7..33e3559dc 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -107,6 +107,7 @@ type Service interface { GetRootDriveChildren(w http.ResponseWriter, r *http.Request) GetDriveItem(w http.ResponseWriter, r *http.Request) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) + CreateLink(w http.ResponseWriter, r *http.Request) Invite(w http.ResponseWriter, r *http.Request) DeletePermission(w http.ResponseWriter, r *http.Request) @@ -201,6 +202,7 @@ func NewService(opts ...Option) (Graph, error) { r.Get("/me/drive/sharedWithMe", svc.ListSharedWithMe) r.Route("/drives/{driveID}/items/{itemID}", func(r chi.Router) { r.Post("/invite", svc.Invite) + r.Post("/createLink", svc.CreateLink) r.Delete("/permissions/{permissionID}", svc.DeletePermission) }) r.Route("/roleManagement/permissions/roleDefinitions", func(r chi.Router) { diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index 26ebf1dac..79cab7c38 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -4,8 +4,6 @@ import ( "context" "errors" "net/http" - "net/url" - "path" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -193,28 +191,13 @@ func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.P } item = *itemptr } - perm := libregraph.Permission{} - perm.SetRoles([]string{}) - perm.SetId(s.Id.OpaqueId) - link := libregraph.SharingLink{} - webURL, err := url.Parse(g.config.Spaces.WebDavBase) + perm, err := g.libreGraphPermissionFromCS3PublicShare(s) if err != nil { - g.logger.Error(). - Err(err). - Str("url", g.config.Spaces.WebDavBase). - Msg("failed to parse webURL base url") + g.logger.Error().Err(err).Interface("Link", s.ResourceId).Msg("could not convert link to libregraph") return driveItems, err } - webURL.Path = path.Join(webURL.Path, "s", s.GetToken()) - link.SetWebUrl(webURL.String()) - perm.SetLink(link) - // set expiration date - if s.GetExpiration() != nil { - perm.SetExpirationDateTime(cs3TimestampToTime(s.GetExpiration())) - } - - item.Permissions = append(item.Permissions, perm) + item.Permissions = append(item.Permissions, *perm) driveItems[resIDStr] = item } @@ -229,6 +212,8 @@ func cs3StatusToErrCode(code rpc.Code) (errcode errorcode.ErrorCode) { errcode = errorcode.AccessDenied case rpc.Code_CODE_NOT_FOUND: errcode = errorcode.ItemNotFound + case rpc.Code_CODE_LOCKED: + errcode = errorcode.ItemIsLocked default: errcode = errorcode.GeneralException } diff --git a/services/graph/pkg/service/v0/sharedbyme_test.go b/services/graph/pkg/service/v0/sharedbyme_test.go index ac128364a..9902f23a6 100644 --- a/services/graph/pkg/service/v0/sharedbyme_test.go +++ b/services/graph/pkg/service/v0/sharedbyme_test.go @@ -23,11 +23,13 @@ import ( 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" + "github.com/owncloud/ocis/v2/services/graph/pkg/linktype" service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" "github.com/stretchr/testify/mock" @@ -36,13 +38,15 @@ import ( var _ = Describe("sharedbyme", 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 + svc service.Service + ctx context.Context + cfg *config.Config + gatewayClient *cs3mocks.GatewayAPIClient + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + eventsPublisher mocks.Publisher + identityBackend *identitymocks.Backend + driveItemCreateLink *libregraph.DriveItemCreateLink + publicShare link.PublicShare rr *httptest.ResponseRecorder ) @@ -113,21 +117,29 @@ var _ = Describe("sharedbyme", func() { }, Expiration: utils.TimeToTS(expiration), } - - publicShare := link.PublicShare{ - Id: &link.PublicShareId{ - OpaqueId: "public-share-id", - }, - Token: "public-share-token", - ResourceId: &provider.ResourceId{ - StorageId: "storageid", - SpaceId: "spaceid", - OpaqueId: "public-share-opaqueid", - }, - } + driveItemCreateLink = &libregraph.DriveItemCreateLink{} BeforeEach(func() { eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) + linkType, err := libregraph.NewSharingLinkTypeFromValue("view") + Expect(err).To(BeNil()) + driveItemCreateLink.Type = linkType + driveItemCreateLink.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(time.Hour)) + permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(*driveItemCreateLink, provider.ResourceType_RESOURCE_TYPE_CONTAINER) + Expect(err).To(BeNil()) + + publicShare = 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: permissions}, + } rr = httptest.NewRecorder()