Skip to content

Commit

Permalink
feat(api): sharing NG links
Browse files Browse the repository at this point in the history
This adds the implementation for the new libregraph create link
endpoint. We also added the needed LibreGraph to CS3 conversions for
both directions.
  • Loading branch information
micbar committed Nov 21, 2023
1 parent 74dc366 commit 2bf00c9
Show file tree
Hide file tree
Showing 17 changed files with 769 additions and 50 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ require (
github.com/onsi/gomega v1.29.0
github.com/open-policy-agent/opa v0.51.0
github.com/orcaman/concurrent-map v1.0.0
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231116165004-6101db024810
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.9
github.com/prometheus/client_golang v1.17.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1773,8 +1773,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb h1:KFnmkGvHY+6k6IZ9I1w5Ia24VbALYms+Y6W7LrsUbsE=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231113143725-09bf34dc9afb/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231116165004-6101db024810 h1:a7ojKQIyMTyqgS+R2w4UAk88RqZ1eIEjoTMoyRV9zU0=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231116165004-6101db024810/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
Expand Down
165 changes: 165 additions & 0 deletions services/graph/pkg/linktype/linktype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package linktype

import (
"errors"

linkv1beta1 "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/storage/utils/grants"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
)

const NoPermissionMatchError = "no matching permission set found"

type LinkType struct {
Permissions *provider.ResourcePermissions
linkType libregraph.SharingLinkType
}

func (l *LinkType) GetPermissions() *provider.ResourcePermissions {
if l != nil {
return l.Permissions
}
return nil
}

// SharingLinkTypeFromCS3Permissions creates a libregraph link type
// It returns a list of libregraph actions when the conversion is not possible
func SharingLinkTypeFromCS3Permissions(permissions *linkv1beta1.PublicSharePermissions) (*libregraph.SharingLinkType, []string) {
linkTypes := GetAvailableLinkTypes()
for _, linkType := range linkTypes {
if grants.PermissionsEqual(linkType.GetPermissions(), permissions.GetPermissions()) {
return &linkType.linkType, nil
}
}
return nil, unifiedrole.CS3ResourcePermissionsToLibregraphActions(*permissions.GetPermissions())
}

func CS3ResourcePermissionsFromSharingLink(createLink libregraph.DriveItemCreateLink, info provider.ResourceType) (*provider.ResourcePermissions, error) {
switch createLink.GetType() {
case "":
return nil, errors.New("link type is empty")
case libregraph.VIEW:
return NewViewLinkPermissionSet().GetPermissions(), nil
case libregraph.EDIT:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return NewFileEditLinkPermissionSet().GetPermissions(), nil
}
return NewFolderEditLinkPermissionSet().GetPermissions(), nil
case libregraph.CREATE_ONLY:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return nil, errors.New(NoPermissionMatchError)
}
return NewFolderDropLinkPermissionSet().GetPermissions(), nil
case libregraph.UPLOAD:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return nil, errors.New(NoPermissionMatchError)
}
return NewFolderUploadLinkPermissionSet().GetPermissions(), nil
case libregraph.INTERNAL:
return NewInternalLinkPermissionSet().GetPermissions(), nil
default:
return nil, errors.New(NoPermissionMatchError)
}
}

func NewInternalLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{},
linkType: libregraph.INTERNAL,
}
}

func NewViewLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
Stat: true,
},
linkType: libregraph.VIEW,
}
}

func NewFileEditLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
// why is this needed?
RestoreRecycleItem: true,
Stat: true,
},
linkType: libregraph.EDIT,
}
}

func NewFolderEditLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
CreateContainer: true,
Delete: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
Move: true,
// why is this needed?
RestoreRecycleItem: true,
Stat: true,
},
linkType: libregraph.EDIT,
}
}

func NewFolderDropLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
Stat: true,
GetPath: true,
CreateContainer: true,
InitiateFileUpload: true,
},
linkType: libregraph.CREATE_ONLY,
}
}

func NewFolderUploadLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
CreateContainer: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
ListRecycle: true,
Stat: true,
},
linkType: libregraph.UPLOAD,
}
}

func GetAvailableLinkTypes() []*LinkType {
return []*LinkType{
NewInternalLinkPermissionSet(),
NewViewLinkPermissionSet(),
NewFolderUploadLinkPermissionSet(),
NewFileEditLinkPermissionSet(),
NewFolderEditLinkPermissionSet(),
NewFolderDropLinkPermissionSet(),
}
}
5 changes: 5 additions & 0 deletions services/graph/pkg/service/v0/errorcode/errorcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const (
Unauthenticated
// PreconditionFailed the request cannot be made and this error response is sent back
PreconditionFailed
// ItemIsLocked The item is locked by another process. Try again later.
ItemIsLocked
)

var errorCodes = [...]string{
Expand All @@ -80,6 +82,7 @@ var errorCodes = [...]string{
"quotaLimitReached",
"unauthenticated",
"preconditionFailed",
"itemIsLocked",
}

// New constructs a new errorcode.Error
Expand Down Expand Up @@ -129,6 +132,8 @@ func (e Error) Render(w http.ResponseWriter, r *http.Request) {
status = http.StatusConflict
case NotAllowed:
status = http.StatusMethodNotAllowed
case ItemIsLocked:
status = http.StatusLocked
default:
status = http.StatusInternalServerError
}
Expand Down
141 changes: 141 additions & 0 deletions services/graph/pkg/service/v0/links.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 2bf00c9

Please sign in to comment.