Bump reva to pull in the ocm changes in reva edge.

Fixes #7003
This commit is contained in:
André Duffeck
2023-10-11 11:40:57 +02:00
parent 9e07f2f0d1
commit b6d5cd78f0
210 changed files with 24545 additions and 5573 deletions
+2 -2
View File
@@ -36,9 +36,9 @@ import (
_ "github.com/cs3org/reva/v2/pkg/datatx/manager/loader"
_ "github.com/cs3org/reva/v2/pkg/group/manager/loader"
_ "github.com/cs3org/reva/v2/pkg/metrics/driver/loader"
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/manager/loader"
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/loader"
_ "github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/loader"
_ "github.com/cs3org/reva/v2/pkg/ocm/share/manager/loader"
_ "github.com/cs3org/reva/v2/pkg/ocm/share/repository/loader"
_ "github.com/cs3org/reva/v2/pkg/permission/manager/loader"
_ "github.com/cs3org/reva/v2/pkg/preferences/loader"
_ "github.com/cs3org/reva/v2/pkg/publicshare/manager/loader"
@@ -32,6 +32,7 @@ import (
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"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
@@ -83,6 +84,10 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s
if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "ocmshare"):
if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
}
log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k)
}
@@ -168,6 +173,21 @@ func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *aut
return checkRelativeReference(ctx, ref, share.ResourceId, client)
}
func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share ocmv1beta1.Share
if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
return err
}
if err := checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil {
return nil
}
// Some services like wopi don't access the shared resource relative to the
// share root but instead relative to the shared resources parent.
return checkRelativeReference(ctx, ref, share.ResourceId, client)
}
// checkRelativeReference checks if the shared resource is being accessed via a relative reference
// e.g.:
// storage: abcd, space: efgh
@@ -171,7 +171,7 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour
appProviderReq := &providerpb.OpenInAppRequest{
ResourceInfo: ri,
ViewMode: providerpb.OpenInAppRequest_ViewMode(vm),
ViewMode: providerpb.ViewMode(vm),
AccessToken: accessToken,
Opaque: opaque,
}
+33 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -42,3 +42,35 @@ func (s *svc) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCore
return res, nil
}
func (s *svc) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.UpdateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.UpdateOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.DeleteOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.DeleteOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -43,6 +43,22 @@ func (s *svc) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInv
return res, nil
}
func (s *svc) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.ListInviteTokens(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListInviteTokens")
}
return res, nil
}
func (s *svc) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
@@ -106,3 +122,19 @@ func (s *svc) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedU
return res, nil
}
func (s *svc) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.DeleteAcceptedUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers")
}
return res, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -25,12 +25,10 @@ import (
"path"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
@@ -53,16 +51,22 @@ func (s *svc) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest
return nil, errors.Wrap(err, "gateway: error calling CreateShare")
}
if s.c.CommitShareToStorageGrant {
addGrantStatus, err := s.addGrant(ctx, req.ResourceId, req.Grant.Grantee, req.Grant.Permissions.Permissions, nil, nil)
if err != nil {
return nil, errors.Wrap(err, "gateway: error adding OCM grant to storage")
}
if addGrantStatus.Code != rpc.Code_CODE_OK {
return &ocm.CreateOCMShareResponse{
Status: addGrantStatus,
}, err
}
status, err := s.addGrant(ctx, req.ResourceId, req.Grantee, req.AccessMethods[0].GetWebdavOptions().Permissions, req.Expiration, nil)
if err != nil {
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
}
switch status.Code {
case rpc.Code_CODE_OK:
s.statCache.RemoveStatContext(ctx, ctxpkg.ContextMustGetUser(ctx).GetId(), req.ResourceId)
case rpc.Code_CODE_UNIMPLEMENTED:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
default:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful")
return &ocm.CreateOCMShareResponse{
Status: status,
}, err
}
return res, nil
@@ -76,44 +80,11 @@ func (s *svc) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest
}, nil
}
// if we need to commit the share, we need the resource it points to.
var share *ocm.Share
if s.c.CommitShareToStorageGrant {
getShareReq := &ocm.GetOCMShareRequest{
Ref: req.Ref,
}
getShareRes, err := c.GetOCMShare(ctx, getShareReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetShare")
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx,
"error getting share when committing to the storage"),
}
return res, nil
}
share = getShareRes.Share
}
res, err := c.RemoveOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveShare")
}
if s.c.CommitShareToStorageGrant {
removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, nil)
if err != nil {
return nil, errors.Wrap(err, "gateway: error removing OCM grant from storage")
}
if removeGrantStatus.Code != rpc.Code_CODE_OK {
return &ocm.RemoveOCMShareResponse{
Status: removeGrantStatus,
}, err
}
}
return res, nil
}
@@ -127,6 +98,7 @@ func (s *svc) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc
func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
@@ -140,10 +112,25 @@ func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc
return res, nil
}
func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareProviderClient")
}
res, err := c.GetOCMShareByToken(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareByToken")
}
return res, nil
}
// TODO(labkode): read GetShare comment.
func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
@@ -160,6 +147,7 @@ func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest)
func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
@@ -176,6 +164,7 @@ func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest
func (s *svc) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
@@ -193,11 +182,49 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive
log := appctx.GetLogger(ctx)
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
// retrieve the current received share
getShareReq := &ocm.GetReceivedOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: req.Share.Id,
},
},
}
getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq)
if err != nil {
log.Error().Err(err).Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: error calling GetReceivedOCMShare",
},
}, nil
}
share := getShareRes.Share
if share == nil {
log.Error().Err(err).Msg("gateway: got a nil share from GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: got a nil share from GetReceivedOCMShare",
},
}, nil
}
res, err := c.UpdateReceivedOCMShare(ctx, req)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
@@ -208,175 +235,17 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive
}, nil
}
// if we don't need to create/delete references then we return early.
if !s.c.CommitShareToStorageGrant {
return res, nil
}
// properties are updated in the order they appear in the field mask
// when an error occurs the request ends and no further fields are updated
for i := range req.UpdateMask.Paths {
switch req.UpdateMask.Paths[i] {
case "state":
switch req.GetShare().GetState() {
case ocm.ShareState_SHARE_STATE_ACCEPTED:
getShareReq := &ocm.GetReceivedOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: req.Share.Share.Id,
},
},
}
getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq)
if err != nil {
log.Err(err).Msg("gateway: error calling GetReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("gateway: error calling GetReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
share := getShareRes.Share
if share == nil {
panic("gateway: error updating a received share: the share is nil")
}
if share.GetShare().ShareType == ocm.Share_SHARE_TYPE_TRANSFER {
srcIdp := share.GetShare().GetOwner().GetIdp()
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: srcIdp,
})
if err != nil {
log.Err(err).Msg("gateway: error calling GetInfoByDomain")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
var srcEndpoint string
var srcEndpointBaseURI string
// target URI scheme will be the webdav endpoint scheme
var srcEndpointScheme string
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
url, err := url.Parse(s.Endpoint.Path)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint " + s.Endpoint.Path)
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
srcEndpoint = url.Host
srcEndpointBaseURI = url.Path
srcEndpointScheme = url.Scheme
break
}
}
var srcToken string
srcTokenOpaque, ok := share.GetShare().Grantee.Opaque.Map["token"]
if !ok {
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
}
switch srcTokenOpaque.Decoder {
case "plain":
srcToken = string(srcTokenOpaque.Value)
default:
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error updating received share"),
}, nil
}
srcPath := path.Join(srcEndpointBaseURI, share.GetShare().Name)
srcTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", srcEndpointScheme, srcToken, srcEndpoint, srcPath)
// get the webdav endpoint of the grantee's idp
var granteeIdp string
if share.GetShare().GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER {
granteeIdp = share.GetShare().GetGrantee().GetUserId().Idp
}
if share.GetShare().GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
granteeIdp = share.GetShare().GetGrantee().GetGroupId().Idp
}
destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
url, err := url.Parse(destWebdavEndpoint)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint " + destWebdavEndpoint)
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
destEndpoint := url.Host
destEndpointBaseURI := url.Path
destEndpointScheme := url.Scheme
destToken := ctxpkg.ContextMustGetToken(ctx)
homeRes, err := s.GetHome(ctx, &provider.GetHomeRequest{})
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
destPath := path.Join(destEndpointBaseURI, homeRes.Path, s.c.DataTransfersFolder, path.Base(share.GetShare().Name))
destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destEndpoint, destPath)
opaqueObj := &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"shareId": {
Decoder: "plain",
Value: []byte(share.GetShare().GetId().OpaqueId),
},
},
}
req := &datatx.CreateTransferRequest{
SrcTargetUri: srcTargetURI,
DestTargetUri: destTargetURI,
Opaque: opaqueObj,
}
res, err := s.CreateTransfer(ctx, req)
if err != nil {
log.Err(err).Msg("gateway: error calling PullTransfer")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, err
}
log.Info().Msgf("gateway: PullTransfer: %v", res.TxInfo)
// do not create an OCM reference, just return
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
createRefStatus, err := s.createOCMReference(ctx, share.Share)
return &ocm.UpdateReceivedOCMShareResponse{
Status: createRefStatus,
}, err
// for a transfer this is handled elsewhere
case ocm.ShareState_SHARE_STATE_PENDING:
// currently no consequences
case ocm.ShareState_SHARE_STATE_REJECTED:
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewUnimplemented(ctx, err, "ocm share rejection not supported at the moment"),
}, nil
// TODO
return res, nil
}
case "mount_point":
// TODO(labkode): implementing updating mount point
@@ -388,12 +257,139 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive
return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported")
}
}
// handle transfer in case it has not already been accepted
if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED {
if share.State == ocm.ShareState_SHARE_STATE_ACCEPTED {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare, share already accepted.")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_FAILED_PRECONDITION,
Message: "Share already accepted.",
},
}, err
}
// get provided destination path
transferDestinationPath, err := s.getTransferDestinationPath(ctx, req)
if err != nil {
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, err
}
}
error := s.handleTransfer(ctx, share, transferDestinationPath)
if error != nil {
log.Err(error).Msg("gateway: error handling transfer in UpdateReceivedShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, error
}
}
return res, nil
}
func (s *svc) handleTransfer(ctx context.Context, share *ocm.ReceivedShare, transferDestinationPath string) error {
log := appctx.GetLogger(ctx)
protocol, ok := s.getTransferProtocol(share)
if !ok {
return errors.New("gateway: unable to retrieve transfer protocol")
}
sourceURI := protocol.SourceUri
// get the webdav endpoint of the grantee's idp
var granteeIdp string
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER {
granteeIdp = share.GetGrantee().GetUserId().Idp
}
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
granteeIdp = share.GetGrantee().GetGroupId().Idp
}
destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
return err
}
destWebdavEndpointURL, err := url.Parse(destWebdavEndpoint)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint \"" + destWebdavEndpoint + "\" into URL structure")
return err
}
destWebdavHost, err := s.getWebdavHost(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare")
return err
}
var dstWebdavURLString string
if strings.Contains(destWebdavHost, "://") {
dstWebdavURLString = destWebdavHost
} else {
dstWebdavURLString = "http://" + destWebdavHost
}
dstWebdavHostURL, err := url.Parse(dstWebdavURLString)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav service host \"" + dstWebdavURLString + "\" into URL structure")
return err
}
destServiceHost := dstWebdavHostURL.Host + dstWebdavHostURL.Path
// optional prefix must only appear in target url path:
// http://...token...@reva.eu/prefix/?name=remote.php/webdav/home/...
destEndpointPath := strings.TrimPrefix(destWebdavEndpointURL.Path, dstWebdavHostURL.Path)
destEndpointScheme := destWebdavEndpointURL.Scheme
destToken := ctxpkg.ContextMustGetToken(ctx)
destPath := path.Join(destEndpointPath, transferDestinationPath, path.Base(share.Name))
destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destServiceHost, destPath)
// var destUri string
req := &datatx.CreateTransferRequest{
SrcTargetUri: sourceURI,
DestTargetUri: destTargetURI,
ShareId: share.Id,
}
res, err := s.CreateTransfer(ctx, req)
if err != nil {
return err
}
log.Info().Msgf("gateway: CreateTransfer: %v", res.TxInfo)
return nil
}
func (s *svc) isTransferShare(share *ocm.ReceivedShare) bool {
_, ok := s.getTransferProtocol(share)
return ok
}
func (s *svc) getTransferDestinationPath(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (string, error) {
log := appctx.GetLogger(ctx)
// the destination path is not part of any protocol, but an opaque field
destPathOpaque, ok := req.GetOpaque().GetMap()["transfer_destination_path"]
if ok {
switch destPathOpaque.Decoder {
case "plain":
if string(destPathOpaque.Value) != "" {
return string(destPathOpaque.Value), nil
}
default:
return "", errtypes.NotSupported("decoder of opaque entry 'transfer_destination_path' not recognized: " + destPathOpaque.Decoder)
}
}
log.Info().Msg("destination path not provided, trying default transfer destination folder")
if s.c.DataTransfersFolder == "" {
return "", errtypes.NotSupported("no destination path provided and default transfer destination folder is not set")
}
return s.c.DataTransfersFolder, nil
}
func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
@@ -407,96 +403,11 @@ func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMSh
return res, nil
}
func (s *svc) createOCMReference(ctx context.Context, share *ocm.Share) (*rpc.Status, error) {
log := appctx.GetLogger(ctx)
var token string
tokenOpaque, ok := share.Grantee.Opaque.Map["token"]
if !ok {
log.Debug().Msg("createOCMReference: token not found in opaque map")
return status.NewNotFound(ctx, "token not found"), nil
}
switch tokenOpaque.Decoder {
case "plain":
token = string(tokenOpaque.Value)
default:
log.Error().Str("decoder", tokenOpaque.Decoder).Msg("createOCMReference: opaque entry decoder not recognized")
return status.NewInternal(ctx, "invalid opaque entry decoder"), nil
}
homeRes, err := s.GetHome(ctx, &provider.GetHomeRequest{})
if err != nil {
log.Error().Err(err).Msg("createOCMReference: error calling GetHome")
return status.NewInternal(ctx, "error updating received share"), nil
}
var refPath, targetURI string
if share.ShareType == ocm.Share_SHARE_TYPE_TRANSFER {
createTransferDir, err := s.CreateContainer(ctx, &provider.CreateContainerRequest{
Ref: &provider.Reference{
Path: path.Join(homeRes.Path, s.c.DataTransfersFolder),
},
})
if err != nil {
log.Error().Err(err).Msg("createOCMReference: error creating transfers directory")
return status.NewInternal(ctx, "error creating transfers directory"), nil
func (s *svc) getTransferProtocol(share *ocm.ReceivedShare) (*ocm.TransferProtocol, bool) {
for _, p := range share.Protocols {
if d, ok := p.Term.(*ocm.Protocol_TransferOptions); ok {
return d.TransferOptions, true
}
if createTransferDir.Status.Code != rpc.Code_CODE_OK && createTransferDir.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
log.Error().Interface("status", createTransferDir.Status).Msg("createOCMReference: error creating transfers directory")
return status.NewInternal(ctx, "error creating transfers directory"), nil
}
refPath = path.Join(homeRes.Path, s.c.DataTransfersFolder, path.Base(share.Name))
targetURI = fmt.Sprintf("datatx://%s@%s?name=%s", token, share.Creator.Idp, share.Name)
} else {
// reference path is the home path + some name on the corresponding
// mesh provider (/home/MyShares/x)
// It is the responsibility of the gateway to resolve these references and merge the response back
// from the main request.
refPath = path.Join(homeRes.Path, s.c.ShareFolder, path.Base(share.Name))
// webdav is the scheme, token@host the opaque part and the share name the query of the URL.
targetURI = fmt.Sprintf("webdav://%s@%s?name=%s", token, share.Creator.Idp, share.Name)
}
log.Info().Msg("mount path will be:" + refPath)
c, p, err := s.findByPath(ctx, refPath)
if err != nil {
log.Error().Err(err).Msg("createOCMReference: could not find storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
var (
root *provider.ResourceId
mountPath string
)
for _, space := range decodeSpaces(p) {
mountPath = decodePath(space)
root = space.Root
break // TODO can there be more than one space for a path?
}
pRef := unwrap(&provider.Reference{Path: refPath}, mountPath, root)
createRefReq := &provider.CreateReferenceRequest{
Ref: pRef,
TargetUri: targetURI,
}
createRefRes, err := c.CreateReference(ctx, createRefReq)
if err != nil {
log.Err(err).Msg("createOCMReference: error calling CreateReference")
return &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
}, nil
}
if createRefRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", createRefRes.Status).Msg("createOCMReference: error calling CreateReference")
return status.NewInternal(ctx, "error updating received share"), nil
}
return status.NewOK(ctx), nil
return nil, false
}
@@ -1030,11 +1030,6 @@ func (s *svc) GetQuota(ctx context.Context, req *gateway.GetQuotaRequest) (*prov
return res, nil
}
func (s *svc) findByPath(ctx context.Context, path string) (provider.ProviderAPIClient, *registry.ProviderInfo, error) {
ref := &provider.Reference{Path: path}
return s.find(ctx, ref)
}
// find looks up the provider that is responsible for the given request
// It will return a client that the caller can use to make the call, as well as the ProviderInfo. It:
// - contains the provider path, which is the mount point of the provider
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -60,21 +60,6 @@ func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webda
}, nil
}
func appendNameQuery(targetURL string, nameQueries ...string) (string, error) {
uri, err := url.Parse(targetURL)
if err != nil {
return "", err
}
q, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return "", err
}
name := append([]string{q["name"][0]}, nameQueries...)
q.Set("name", path.Join(name...))
uri.RawQuery = q.Encode()
return uri.String(), nil
}
func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
@@ -89,3 +74,33 @@ func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, err
}
return "", errtypes.NotFound(domain)
}
func (s *svc) getWebdavHost(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
})
if err != nil {
return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
return s.Host, nil
}
}
return "", errtypes.NotFound(domain)
}
func appendNameQuery(targetURL string, nameQueries ...string) (string, error) {
uri, err := url.Parse(targetURL)
if err != nil {
return "", err
}
q, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return "", err
}
name := append([]string{q["name"][0]}, nameQueries...)
q.Set("name", path.Join(name...))
uri.RawQuery = q.Encode()
return uri.String(), nil
}
+49 -99
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,20 +20,19 @@ package ocmcore
import (
"context"
"encoding/json"
"fmt"
"time"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
"github.com/cs3org/reva/v2/pkg/rgrpc"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"google.golang.org/grpc"
)
@@ -48,10 +47,10 @@ type config struct {
type service struct {
conf *config
sm share.Manager
repo share.Repository
}
func (c *config) init() {
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
@@ -61,39 +60,28 @@ func (s *service) Register(ss *grpc.Server) {
ocmcore.RegisterOcmCoreAPIServer(ss, s)
}
func getShareManager(c *config) (share.Manager, error) {
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver not found: %s", c.Driver))
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new ocm core svc
// New creates a new ocm core svc.
func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c)
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
service := &service{
conf: c,
sm: sm,
conf: &c,
repo: repo,
}
return service, nil
@@ -107,91 +95,53 @@ func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"}
}
// CreateOCMCoreShare is called when an OCM request comes into this reva instance from
// CreateOCMCoreShare is called when an OCM request comes into this reva instance from.
func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) {
resource := &provider.ResourceId{
StorageId: "remote",
OpaqueId: req.Name,
if req.ShareType != ocm.ShareType_SHARE_TYPE_USER {
return nil, errtypes.NotSupported("share type not supported")
}
var resourcePermissions *provider.ResourcePermissions
permOpaque, ok := req.Protocol.Opaque.Map["permissions"]
if !ok {
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "resource permissions not set"),
}, nil
}
switch permOpaque.Decoder {
case "json":
err := json.Unmarshal(permOpaque.Value, &resourcePermissions)
if err != nil {
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error decoding resource permissions"),
}, nil
}
default:
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "invalid opaque entry decoder"),
}, nil
now := &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Unix()),
}
var token string
tokenOpaque, ok := req.Protocol.Opaque.Map["token"]
if !ok {
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "token not set"),
}, nil
}
switch tokenOpaque.Decoder {
case "plain":
token = string(tokenOpaque.Value)
default:
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "invalid opaque entry decoder"),
}, nil
}
grant := &ocm.ShareGrant{
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
// For now, we only support user shares.
// TODO (ishank011): To be updated once this is decided.
Id: &provider.Grantee_UserId{UserId: req.ShareWith},
// passing this in grant.Grantee.Opaque because ShareGrant itself doesn't have a root opaque.
Opaque: &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
"remoteShareId": {
Decoder: "plain",
Value: []byte(req.ProviderId),
},
},
share, err := s.repo.StoreReceivedShare(ctx, &ocm.ReceivedShare{
RemoteShareId: req.ResourceId,
Name: req.Name,
Grantee: &providerpb.Grantee{
Type: providerpb.GranteeType_GRANTEE_TYPE_USER,
Id: &providerpb.Grantee_UserId{
UserId: req.ShareWith,
},
},
Permissions: &ocm.SharePermissions{
Permissions: resourcePermissions,
},
}
var shareType ocm.Share_ShareType
switch req.Protocol.Name {
case "datatx":
shareType = ocm.Share_SHARE_TYPE_TRANSFER
default:
shareType = ocm.Share_SHARE_TYPE_REGULAR
}
share, err := s.sm.Share(ctx, resource, grant, req.Name, nil, "", req.Owner, token, shareType)
ResourceType: req.ResourceType,
ShareType: req.ShareType,
Owner: req.Owner,
Creator: req.Sender,
Protocols: req.Protocols,
Ctime: now,
Mtime: now,
Expiration: req.Expiration,
State: ocm.ShareState_SHARE_STATE_PENDING,
})
if err != nil {
// TODO: identify errors
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error creating ocm core share"),
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
res := &ocmcore.CreateOCMCoreShareResponse{
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewOK(ctx),
Id: share.Id.OpaqueId,
Created: share.Ctime,
}
return res, nil
}, nil
}
func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,14 +20,24 @@ package ocminvitemanager
import (
"context"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/client"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/ocm/invite/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/cs3org/reva/v2/pkg/rgrpc"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/mitchellh/mapstructure"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
@@ -37,58 +47,77 @@ func init() {
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TokenExpiration string `mapstructure:"token_expiration"`
OCMClientTimeout int `mapstructure:"ocm_timeout"`
OCMClientInsecure bool `mapstructure:"ocm_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
tokenExpiration time.Duration
}
type service struct {
conf *config
im invite.Manager
conf *config
repo invite.Repository
ocmClient *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (c *config) init() {
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.TokenExpiration == "" {
c.TokenExpiration = "24h"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
invitepb.RegisterInviteAPIServer(ss, s)
}
func getInviteManager(c *config) (invite.Manager, error) {
func getInviteRepository(c *config) (invite.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
// New creates a new OCM invite manager svc.
func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
return c, nil
}
// New creates a new OCM invite manager svc
func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
c, err := parseConfig(m)
p, err := time.ParseDuration(c.TokenExpiration)
if err != nil {
return nil, err
}
c.init()
c.tokenExpiration = p
im, err := getInviteManager(c)
repo, err := getInviteRepository(&c)
if err != nil {
return nil, err
}
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
service := &service{
conf: c,
im: im,
conf: &c,
repo: repo,
ocmClient: client.New(&client.Config{
Timeout: time.Duration(c.OCMClientTimeout) * time.Second,
Insecure: c.OCMClientInsecure,
}),
gatewaySelector: gatewaySelector,
}
return service, nil
}
@@ -98,12 +127,14 @@ func (s *service) Close() error {
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite"}
return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"}
}
func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
token, err := s.im.GenerateToken(ctx)
if err != nil {
user := ctxpkg.ContextMustGetUser(ctx)
token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description)
if err := s.repo.AddToken(ctx, token); err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, "error generating invite token"),
}, nil
@@ -115,34 +146,175 @@ func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.Generat
}, nil
}
func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
err := s.im.ForwardInvite(ctx, req.InviteToken, req.OriginSystemProvider)
func (s *service) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
tokens, err := s.repo.ListTokens(ctx, user.Id)
if err != nil {
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, "error forwarding invite"),
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error listing tokens"),
}, nil
}
return &invitepb.ForwardInviteResponse{
Status: status.NewOK(ctx),
return &invitepb.ListInviteTokensResponse{
Status: status.NewOK(ctx),
InviteTokens: tokens,
}, nil
}
func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider())
if err != nil {
return nil, err
}
remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(),
RecipientProvider: s.conf.ProviderDomain,
UserID: user.GetId().GetOpaqueId(),
Email: user.GetMail(),
Name: user.GetDisplayName(),
})
if err != nil {
switch {
case errors.Is(err, client.ErrTokenInvalid):
return &invitepb.ForwardInviteResponse{
Status: status.NewInvalid(ctx, "token not valid"),
}, nil
case errors.Is(err, client.ErrTokenNotFound):
return &invitepb.ForwardInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
case errors.Is(err, client.ErrUserAlreadyAccepted):
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &invitepb.ForwardInviteResponse{
Status: status.NewPermissionDenied(ctx, err, err.Error()),
}, nil
default:
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
// create a link between the user that accepted the share (in ctx)
// and the remote one (the initiator), so at the end of the invitation workflow they
// know each other
remoteUserID := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_FEDERATED,
Idp: req.GetOriginSystemProvider().Domain,
OpaqueId: remoteUser.UserID,
}
if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
Id: remoteUserID,
Mail: remoteUser.Email,
DisplayName: remoteUser.Name,
}); err != nil {
if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
// skip error if user was already accepted
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
return &invitepb.ForwardInviteResponse{
Status: status.NewOK(ctx),
UserId: remoteUserID,
Email: remoteUser.Email,
DisplayName: remoteUser.Name,
}, nil
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
err := s.im.AcceptInvite(ctx, req.InviteToken, req.RemoteUser)
token, err := s.repo.GetToken(ctx, req.InviteToken.Token)
if err != nil {
if errors.Is(err, invite.ErrTokenNotFound) {
return &invitepb.AcceptInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
if !isTokenValid(token) {
return &invitepb.AcceptInviteResponse{
Status: status.NewInvalid(ctx, "token is not valid"),
}, nil
}
initiator, err := s.getUserInfo(ctx, token.UserId)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, "error accepting invite"),
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), req.GetRemoteUser()); err != nil {
if errors.Is(err, invite.ErrUserAlreadyAccepted) {
return &invitepb.AcceptInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewOK(ctx),
Status: status.NewOK(ctx),
UserId: initiator.GetId(),
Email: initiator.Mail,
DisplayName: initiator.DisplayName,
}, nil
}
func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) {
gw, err := s.gatewaySelector.Next()
if err != nil {
return nil, nil
}
res, err := gw.GetUser(ctx, &userpb.GetUserRequest{
UserId: id,
})
if err != nil {
return nil, err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(res.Status.Message)
}
return res.User, nil
}
func isTokenValid(token *invitepb.InviteToken) bool {
return time.Now().Unix() < int64(token.Expiration.Seconds)
}
func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
remoteUser, err := s.im.GetAcceptedUser(ctx, req.RemoteUserId)
user, ok := getUserFilter(ctx, req)
if !ok {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInvalidArg(ctx, "user not found"),
}, nil
}
remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId())
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInternal(ctx, "error fetching remote user details"),
@@ -155,11 +327,34 @@ func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAccepted
}, nil
}
func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) {
user, ok := ctxpkg.ContextGetUser(ctx)
if ok {
return user, true
}
if req.Opaque == nil || req.Opaque.Map == nil {
return nil, false
}
v, ok := req.Opaque.Map["user-filter"]
if !ok {
return nil, false
}
var u userpb.UserId
if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil {
return nil, false
}
return &userpb.User{Id: &u}, true
}
func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
acceptedUsers, err := s.im.FindAcceptedUsers(ctx, req.Filter)
user := ctxpkg.ContextMustGetUser(ctx)
acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter())
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, "error finding remote users"),
Status: status.NewInternal(ctx, "error finding remote users: "+err.Error()),
}, nil
}
@@ -168,3 +363,16 @@ func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAccep
AcceptedUsers: acceptedUsers,
}, nil
}
func (s *service) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteRemoteUser(ctx, user.Id, req.RemoteUserId); err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error deleting remote users: "+err.Error()),
}, nil
}
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewOK(ctx),
}, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package token
package ocminvitemanager
import (
"time"
@@ -25,33 +25,21 @@ import (
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// DefaultExpirationTime is the expiration time to be used when unspecified in the config.
const DefaultExpirationTime = "24h"
// CreateToken creates a InviteToken object for the userID indicated by userID.
func CreateToken(expiration string, userID *userpb.UserId) (*invitepb.InviteToken, error) {
// Parse time of expiration
duration, err := time.ParseDuration(expiration)
if err != nil {
return nil, errors.Wrap(err, "error parsing time of expiration")
}
func CreateToken(expiration time.Duration, userID *userpb.UserId, description string) *invitepb.InviteToken {
tokenID := uuid.New().String()
now := time.Now()
expirationTime := now.Add(duration)
expirationTime := now.Add(expiration)
token := invitepb.InviteToken{
return &invitepb.InviteToken{
Token: tokenID,
UserId: userID,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(expirationTime.Unix()),
Nanos: 0,
},
Description: description,
}
return &token, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,14 +20,33 @@ package ocmshareprovider
import (
"context"
"fmt"
"net/url"
"path/filepath"
"strings"
"text/template"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/ocmd"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/client"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
"github.com/cs3org/reva/v2/pkg/rgrpc"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/mitchellh/mapstructure"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/storage/utils/walker"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
@@ -37,58 +56,85 @@ func init() {
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
ClientTimeout int `mapstructure:"client_timeout"`
ClientInsecure bool `mapstructure:"client_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
WebDAVEndpoint string `mapstructure:"webdav_endpoint" validate:"required"`
WebappTemplate string `mapstructure:"webapp_template"`
}
type service struct {
conf *config
sm share.Manager
conf *config
repo share.Repository
client *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
webappTmpl *template.Template
walker walker.Walker
}
func (c *config) init() {
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.ClientTimeout == 0 {
c.ClientTimeout = 10
}
if c.WebappTemplate == "" {
c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
ocm.RegisterOcmAPIServer(ss, s)
}
func getShareManager(c *config) (share.Manager, error) {
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new ocm share provider svc
// New creates a new ocm share provider svc.
func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
c, err := parseConfig(m)
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c)
client := client.New(&client.Config{
Timeout: time.Duration(c.ClientTimeout) * time.Second,
Insecure: c.ClientInsecure,
})
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
tpl, err := template.New("webapp_template").Parse(c.WebappTemplate)
if err != nil {
return nil, err
}
walker := walker.NewWalker(gatewaySelector)
service := &service{
conf: c,
sm: sm,
conf: &c,
repo: repo,
client: client,
gatewaySelector: gatewaySelector,
webappTmpl: tpl,
walker: walker,
}
return service, nil
@@ -99,98 +145,240 @@ func (s *service) Close() error {
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"}
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func formatOCMUser(u *userpb.UserId) string {
return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp)
}
func getResourceType(info *providerpb.ResourceInfo) string {
switch info.Type {
case providerpb.ResourceType_RESOURCE_TYPE_FILE:
return "file"
case providerpb.ResourceType_RESOURCE_TYPE_CONTAINER:
return "folder"
}
return "unknown"
}
func (s *service) webdavURL(ctx context.Context, share *ocm.Share) string {
// the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token
p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/dav/ocm", share.Token)
return p
}
func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV {
var perms []string
if m.WebdavOptions.Permissions.InitiateFileDownload {
perms = append(perms, "read")
}
if m.WebdavOptions.Permissions.InitiateFileUpload {
perms = append(perms, "write")
}
return &ocmd.WebDAV{
Permissions: perms,
URL: s.webdavURL(ctx, share),
SharedSecret: share.Token,
}
}
func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp {
var b strings.Builder
if err := s.webappTmpl.Execute(&b, share); err != nil {
return nil
}
return &ocmd.Webapp{
URITemplate: b.String(),
}
}
func (s *service) getDataTransferProtocol(ctx context.Context, share *ocm.Share) *ocmd.Datatx {
var size uint64
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil
}
// get the path of the share
statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{
Ref: &providerpb.Reference{
ResourceId: share.ResourceId,
},
})
if err != nil {
return nil
}
err = s.walker.Walk(ctx, statRes.GetInfo().GetId(), func(path string, info *providerpb.ResourceInfo, err error) error {
if info.Type == providerpb.ResourceType_RESOURCE_TYPE_FILE {
size += info.Size
}
return nil
})
if err != nil {
return nil
}
return &ocmd.Datatx{
SourceURI: s.webdavURL(ctx, share),
Size: size,
}
}
func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols {
var p ocmd.Protocols
for _, m := range share.AccessMethods {
var newProtocol ocmd.Protocol
switch t := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
newProtocol = s.getWebdavProtocol(ctx, share, t)
case *ocm.AccessMethod_WebappOptions:
newProtocol = s.getWebappProtocol(share)
case *ocm.AccessMethod_TransferOptions:
newProtocol = s.getDataTransferProtocol(ctx, share)
}
if newProtocol != nil {
p = append(p, newProtocol)
}
}
return p
}
// Note: this is for outgoing OCM shares
// This function is used when you for instance
// call `ocm-share-create` in reva-cli.
// For incoming OCM shares from internal/http/services/ocmd/shares.go
// there is the very similar but slightly different function
// CreateOCMCoreShare (the "Core" somehow means "incoming").
// So make sure to keep in mind the difference between this file for outgoing:
// internal/grpc/services/ocmshareprovider/ocmshareprovider.go
// and the other one for incoming:
// internal/grpc/service/ocmcore/ocmcore.go
// Both functions end up calling the same s.sm.Share function
// on the OCM share manager:
// pkg/ocm/share/manager/{json|nextcloud|...}
func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) {
if req.Opaque == nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "can't find resource permissions"),
}, nil
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{
Ref: &providerpb.Reference{
ResourceId: req.ResourceId,
},
})
if err != nil {
return nil, err
}
var permissions string
permOpaque, ok := req.Opaque.Map["permissions"]
if !ok {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "resource permissions not set"),
}, nil
}
switch permOpaque.Decoder {
case "plain":
permissions = string(permOpaque.Value)
default:
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "invalid opaque entry decoder"),
}, nil
}
var name string
nameOpaque, ok := req.Opaque.Map["name"]
if !ok {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "resource name not set"),
}, nil
}
switch nameOpaque.Decoder {
case "plain":
name = string(nameOpaque.Value)
default:
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "invalid opaque entry decoder"),
}, nil
}
// discover share type
sharetype := ocm.Share_SHARE_TYPE_REGULAR
// FIXME: https://github.com/cs3org/reva/issues/2402
protocol, ok := req.Opaque.Map["protocol"]
if ok {
switch protocol.Decoder {
case "plain":
if string(protocol.Value) == "datatx" {
sharetype = ocm.Share_SHARE_TYPE_TRANSFER
}
default:
if statRes.Status.Code != rpc.Code_CODE_OK {
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "error creating share"),
Status: status.NewNotFound(ctx, statRes.Status.Message),
}, nil
}
// token = protocol FIXME!
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, statRes.Status.Message),
}, nil
}
var sharedSecret string
share, err := s.sm.Share(ctx, req.ResourceId, req.Grant, name, req.RecipientMeshProvider, permissions, nil, sharedSecret, sharetype)
info := statRes.Info
user := ctxpkg.ContextMustGetUser(ctx)
tkn := utils.RandString(32)
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
ocmshare := &ocm.Share{
Token: tkn,
Name: filepath.Base(info.Path),
ResourceId: req.ResourceId,
Grantee: req.Grantee,
ShareType: ocm.ShareType_SHARE_TYPE_USER,
Owner: info.Owner,
Creator: user.Id,
Ctime: ts,
Mtime: ts,
Expiration: req.Expiration,
AccessMethods: req.AccessMethods,
}
ocmshare, err = s.repo.StoreShare(ctx, ocmshare)
if err != nil {
if errors.Is(err, share.ErrShareAlreadyExisting) {
return &ocm.CreateOCMShareResponse{
Status: status.NewAlreadyExists(ctx, err, "share already exists"),
}, nil
}
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
ocmEndpoint, err := getOCMEndpoint(req.RecipientMeshProvider)
if err != nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "error creating share"),
Status: status.NewInvalidArg(ctx, "the selected provider does not have an OCM endpoint"),
}, nil
}
newShareReq := &client.NewShareRequest{
ShareWith: formatOCMUser(req.Grantee.GetUserId()),
Name: ocmshare.Name,
ProviderID: ocmshare.Id.OpaqueId,
Owner: formatOCMUser(&userpb.UserId{
OpaqueId: info.Owner.OpaqueId,
Idp: s.conf.ProviderDomain, // FIXME: this is not generally true in case of resharing
}),
Sender: formatOCMUser(&userpb.UserId{
OpaqueId: user.Id.OpaqueId,
Idp: s.conf.ProviderDomain,
}),
SenderDisplayName: user.DisplayName,
ShareType: "user",
ResourceType: getResourceType(info),
Protocols: s.getProtocols(ctx, ocmshare),
}
if req.Expiration != nil {
newShareReq.Expiration = req.Expiration.Seconds
}
newShareRes, err := s.client.NewShare(ctx, ocmEndpoint, newShareReq)
if err != nil {
switch {
case errors.Is(err, client.ErrInvalidParameters):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
default:
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
res := &ocm.CreateOCMShareResponse{
Status: status.NewOK(ctx),
Share: share,
Status: status.NewOK(ctx),
Share: ocmshare,
RecipientDisplayName: newShareRes.RecipientDisplayName,
}
return res, nil
}
func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
err := s.sm.Unshare(ctx, req.Ref)
if err != nil {
// TODO (gdelmont): notify the remote provider using the /notification ocm endpoint
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.RemoveOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error removing share"),
}, nil
@@ -202,8 +390,18 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq
}
func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
share, err := s.sm.GetShare(ctx, req.Ref)
// if the request is by token, the user does not need to be in the ctx
var user *userpb.User
if req.Ref.GetToken() == "" {
user = ctxpkg.ContextMustGetUser(ctx)
}
ocmshare, err := s.repo.GetShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
@@ -211,12 +409,36 @@ func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest)
return &ocm.GetOCMShareResponse{
Status: status.NewOK(ctx),
Share: share,
Share: ocmshare,
}, nil
}
func (s *service) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
ocmshare, err := s.repo.GetShare(ctx, nil, &ocm.ShareReference{
Spec: &ocm.ShareReference_Token{
Token: req.Token,
},
})
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}, nil
}
func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListShares(ctx, user, req.Filters)
if err != nil {
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing shares"),
@@ -231,8 +453,19 @@ func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesReque
}
func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
_, err := s.sm.UpdateShare(ctx, req.Ref, req.Field.GetPermissions()) // TODO(labkode): check what to update
user := ctxpkg.ContextMustGetUser(ctx)
if len(req.Field) == 0 {
return &ocm.UpdateOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
_, err := s.repo.UpdateShare(ctx, user, req.Ref, req.Field...)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error updating share"),
}, nil
@@ -245,7 +478,8 @@ func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareReq
}
func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
shares, err := s.sm.ListReceivedShares(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListReceivedShares(ctx, user)
if err != nil {
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
@@ -260,8 +494,14 @@ func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceiv
}
func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) {
_, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask) // TODO(labkode): check what to update
user := ctxpkg.ContextMustGetUser(ctx)
_, err := s.repo.UpdateReceivedShare(ctx, user, req.Share, req.UpdateMask)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error updating received share"),
}, nil
@@ -274,8 +514,14 @@ func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateRec
}
func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
share, err := s.sm.GetReceivedShare(ctx, req.Ref)
user := ctxpkg.ContextMustGetUser(ctx)
ocmshare, err := s.repo.GetReceivedShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
@@ -283,7 +529,7 @@ func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedO
res := &ocm.GetReceivedOCMShareResponse{
Status: status.NewOK(ctx),
Share: share,
Share: ocmshare,
}
return res, nil
}
@@ -30,10 +30,12 @@ import (
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rgrpc"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
@@ -105,13 +107,10 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
}
func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest) (*provider.SetArbitraryMetadataResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.SetArbitraryMetadataResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -127,13 +126,10 @@ func (s *service) UnsetArbitraryMetadata(ctx context.Context, req *provider.Unse
// SetLock puts a lock on the given reference
func (s *service) SetLock(ctx context.Context, req *provider.SetLockRequest) (*provider.SetLockResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.SetLockResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -145,13 +141,10 @@ func (s *service) SetLock(ctx context.Context, req *provider.SetLockRequest) (*p
// GetLock returns an existing lock on the given reference
func (s *service) GetLock(ctx context.Context, req *provider.GetLockRequest) (*provider.GetLockResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.GetLockResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -163,13 +156,10 @@ func (s *service) GetLock(ctx context.Context, req *provider.GetLockRequest) (*p
// RefreshLock refreshes an existing lock on the given reference
func (s *service) RefreshLock(ctx context.Context, req *provider.RefreshLockRequest) (*provider.RefreshLockResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.RefreshLockResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -181,13 +171,10 @@ func (s *service) RefreshLock(ctx context.Context, req *provider.RefreshLockRequ
// Unlock removes an existing lock from the given reference
func (s *service) Unlock(ctx context.Context, req *provider.UnlockRequest) (*provider.UnlockResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.UnlockResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -220,26 +207,16 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia
return s.initiateFileDownload(ctx, req)
}
func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, string, *link.PublicShare, *rpc.Status, error) {
func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, *provider.ResourceInfo, string, error) {
log := appctx.GetLogger(ctx)
share, ok := extractLinkFromScope(ctx)
if !ok {
return nil, "", nil, nil, gstatus.Errorf(codes.NotFound, "share or token not found")
}
// the share is minimally populated, we need more than the token
// look up complete share
ls, shareInfo, st, err := s.resolveToken(ctx, share.Token)
switch {
case err != nil:
return nil, "", nil, nil, err
case st != nil:
return nil, "", nil, st, nil
info, _, _, token, err := s.extractLinkFromScope(ctx)
if err != nil {
return nil, nil, "", err
}
var path string
switch shareInfo.Type {
switch info.Type {
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
// folders point to the folder -> path needs to be added
path = utils.MakeRelativePath(ref.Path)
@@ -252,37 +229,34 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.
}
cs3Ref := &provider.Reference{
ResourceId: shareInfo.Id,
ResourceId: info.Id,
Path: path,
}
log.Debug().
Interface("sourceRef", ref).
Interface("cs3Ref", cs3Ref).
Interface("share", ls).
Str("tkn", share.Token).
Str("originalPath", shareInfo.Path).
Str("tkn", token).
Str("originalPath", info.Path).
Str("relativePath", path).
Msg("translatePublicRefToCS3Ref")
return cs3Ref, share.Token, ls, nil, nil
return cs3Ref, info, token, nil
}
func (s *service) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) {
cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.InitiateFileDownloadResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileDownload:
case info.PermissionSet == nil || !info.PermissionSet.InitiateFileDownload:
return &provider.InitiateFileDownloadResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileDownload permission"),
}, nil
}
dReq := &provider.InitiateFileDownloadRequest{
Ref: cs3Ref,
Ref: ref,
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -325,15 +299,13 @@ func (s *service) initiateFileDownload(ctx context.Context, req *provider.Initia
}
func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) {
cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.InitiateFileUploadResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileUpload:
case info.PermissionSet == nil || !info.PermissionSet.InitiateFileUpload:
return &provider.InitiateFileUploadResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileUpload permission"),
}, nil
@@ -443,12 +415,19 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
}
}
// if there is no public scope there are no publicstorage spaces
share, ok := extractLinkFromScope(ctx)
if !ok {
return &provider.ListStorageSpacesResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_OK},
}, nil
info, _, grantee, token, err := s.extractLinkFromScope(ctx)
if err != nil {
switch err.(type) {
case errtypes.NotFound:
// if there is no public scope there are no publicstorage spaces
return &provider.ListStorageSpacesResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_OK},
}, nil
default:
return &provider.ListStorageSpacesResponse{
Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL},
}, nil
}
}
if len(spaceTypes) == 0 {
@@ -466,7 +445,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
case "grant":
// when a list storage space with the resourceid of an external
// resource is made we may have a grant for it
root := share.ResourceId
root := info.Id
if spaceID != nil && !utils.ResourceIDEqual(spaceID, root) {
// none of our business
continue
@@ -477,7 +456,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
OpaqueId: storagespace.FormatResourceID(*root),
},
SpaceType: "grant",
Owner: &userv1beta1.User{Id: share.Owner},
Owner: &userv1beta1.User{Id: grantee},
// the publicstorageprovider keeps track of mount points
Root: root,
}
@@ -487,15 +466,15 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
root := &provider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: share.Token, // the link share has no id, only the token
OpaqueId: token, // the link share has no id, only the token
}
if spaceID != nil {
switch {
case utils.ResourceIDEqual(spaceID, root):
// we have a virtual node
case utils.ResourceIDEqual(spaceID, share.ResourceId):
case utils.ResourceIDEqual(spaceID, info.Id):
// we have a mount point
root = share.ResourceId
root = info.Id
default:
// none of our business
continue
@@ -506,7 +485,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
OpaqueId: storagespace.FormatResourceID(*root),
},
SpaceType: "mountpoint",
Owner: &userv1beta1.User{Id: share.Owner}, // FIXME actually, the mount point belongs to no one?
Owner: &userv1beta1.User{Id: grantee}, // FIXME actually, the mount point belongs to no one?
// the publicstorageprovider keeps track of mount points
Root: root,
}
@@ -517,27 +496,44 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora
return res, nil
}
func extractLinkFromScope(ctx context.Context) (*link.PublicShare, bool) {
func (s *service) extractLinkFromScope(ctx context.Context) (*provider.ResourceInfo, interface{}, *userv1beta1.UserId, string, error) {
scopes, ok := ctxpkg.ContextGetScopes(ctx)
if !ok {
return nil, false
return nil, nil, nil, "", errtypes.NotFound("No scopes found in context")
}
var share *link.PublicShare
for k, v := range scopes {
if strings.HasPrefix(k, "publicshare:") && v.Resource.Decoder == "json" {
share = &link.PublicShare{}
if strings.HasPrefix(k, "ocmshare:") && v.Resource.Decoder == "json" {
share := &ocm.Share{}
err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, share)
if err != nil {
continue
return nil, nil, nil, "", errtypes.InternalError("failed to unmarshal public share")
}
// the share is minimally populated, we need more than the token
// look up complete share
info, resolvedShare, err := s.resolveToken(ctx, share)
if err != nil {
return nil, nil, nil, "", err
}
return info, resolvedShare, share.Owner, share.Token, nil
} else if strings.HasPrefix(k, "publicshare:") && v.Resource.Decoder == "json" {
share := &link.PublicShare{}
err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, share)
if err != nil {
return nil, nil, nil, "", errtypes.InternalError("failed to unmarshal public share")
}
// the share is minimally populated, we need more than the token
// look up complete share
info, resolvedShare, err := s.resolveToken(ctx, share)
if err != nil {
return nil, nil, nil, "", err
}
return info, resolvedShare, share.Owner, share.Token, nil
}
}
if share == nil {
return nil, false
}
return share, true
return nil, nil, nil, "", errtypes.NotFound("No public storage info found in scopes")
}
func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) {
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
}
@@ -555,15 +551,13 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta
Value: attribute.StringValue(req.Ref.String()),
})
cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.CreateContainerResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.CreateContainer:
case info.PermissionSet == nil || !info.PermissionSet.CreateContainer:
return &provider.CreateContainerResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant CreateContainer permission"),
}, nil
@@ -591,13 +585,10 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta
}
func (s *service) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) {
ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
if err != nil {
return &provider.TouchFileResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
@@ -616,15 +607,13 @@ func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro
Value: attribute.StringValue(req.Ref.String()),
})
cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.DeleteResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err),
}, nil
case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Delete:
case info.PermissionSet == nil || !info.PermissionSet.Delete:
return &provider.DeleteResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant Delete permission"),
}, nil
@@ -666,27 +655,22 @@ func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provide
},
)
cs3RefSource, tknSource, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Source)
cs3RefSource, info, tknSource, err := s.translatePublicRefToCS3Ref(ctx, req.Source)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.MoveResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve source reference", err),
}, nil
case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Move:
case info.PermissionSet == nil || !info.PermissionSet.Move:
return &provider.MoveResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant Move permission"),
}, nil
}
// FIXME: maybe there's a shortcut possible here using the source path
cs3RefDestination, tknDest, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Destination)
switch {
case err != nil:
return nil, err
case st != nil:
cs3RefDestination, _, tknDest, err := s.translatePublicRefToCS3Ref(ctx, req.Destination)
if err != nil {
return &provider.MoveResponse{
Status: st,
Status: status.NewStatusFromErrType(ctx, "failed to resolve destination reference", err),
}, nil
}
@@ -728,40 +712,31 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide
Value: attribute.StringValue(req.Ref.String()),
})
share, ok := extractLinkFromScope(ctx)
if !ok {
return &provider.StatResponse{
Status: status.NewNotFound(ctx, "share or token not found"),
}, nil
info, share, _, token, err := s.extractLinkFromScope(ctx)
if err != nil {
switch err.(type) {
case errtypes.NotFound:
return &provider.StatResponse{
Status: status.NewNotFound(ctx, "share or token not found"),
}, nil
default:
return &provider.StatResponse{
Status: status.NewInternal(ctx, "share or token not found"),
}, nil
}
}
// the share is minimally populated, we need more than the token
// look up complete share
share, shareInfo, st, err := s.resolveToken(ctx, share.Token)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.StatResponse{
Status: st,
}, nil
case share.GetPermissions() == nil || !share.GetPermissions().Permissions.Stat:
return &provider.StatResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant Stat permission"),
}, nil
}
if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE || req.Ref.Path == "" {
if info.Type == provider.ResourceType_RESOURCE_TYPE_FILE || req.Ref.Path == "" {
res := &provider.StatResponse{
Status: status.NewOK(ctx),
Info: shareInfo,
Info: info,
}
s.augmentStatResponse(ctx, res, shareInfo, share, share.Token)
s.augmentStatResponse(ctx, res.Info, info, share, token)
return res, nil
}
ref := &provider.Reference{
ResourceId: share.ResourceId,
ResourceId: info.Id,
Path: utils.MakeRelativePath(req.Ref.Path),
}
@@ -776,38 +751,38 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide
}, nil
}
s.augmentStatResponse(ctx, statResponse, shareInfo, share, share.Token)
s.augmentStatResponse(ctx, statResponse.Info, info, share, token)
return statResponse, nil
}
func (s *service) augmentStatResponse(ctx context.Context, res *provider.StatResponse, shareInfo *provider.ResourceInfo, share *link.PublicShare, tkn string) {
func (s *service) augmentStatResponse(ctx context.Context, statInfo *provider.ResourceInfo, shareInfo *provider.ResourceInfo, share interface{}, tkn string) {
// prevent leaking internal paths
if res.Info != nil {
if err := addShare(res.Info, share); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", res.Info).Msg("error when adding share")
if statInfo != nil {
if err := addShare(statInfo, share); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", statInfo).Msg("error when adding share")
}
var sharePath string
if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE {
sharePath = path.Base(shareInfo.Path)
} else {
sharePath = strings.TrimPrefix(res.Info.Path, shareInfo.Path)
sharePath = strings.TrimPrefix(statInfo.Path, shareInfo.Path)
}
res.Info.Path = path.Join("/", sharePath)
filterPermissions(res.Info.PermissionSet, share.GetPermissions().Permissions)
statInfo.Path = path.Join("/", sharePath)
filterPermissions(statInfo.PermissionSet, shareInfo.PermissionSet)
}
}
func addShare(i *provider.ResourceInfo, ls *link.PublicShare) error {
func addShare(i *provider.ResourceInfo, share interface{}) error {
if i.Opaque == nil {
i.Opaque = &typesv1beta1.Opaque{}
}
if i.Opaque.Map == nil {
i.Opaque.Map = map[string]*typesv1beta1.OpaqueEntry{}
}
val, err := json.Marshal(ls)
val, err := json.Marshal(share)
if err != nil {
return err
}
@@ -820,25 +795,20 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest,
}
func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) {
share, ok := extractLinkFromScope(ctx)
if !ok {
return &provider.ListContainerResponse{
Status: status.NewNotFound(ctx, "share or token not found"),
}, nil
info, share, _, _, err := s.extractLinkFromScope(ctx)
if err != nil {
switch err.(type) {
case errtypes.NotFound:
return &provider.ListContainerResponse{
Status: status.NewNotFound(ctx, "share or token not found"),
}, nil
default:
return &provider.ListContainerResponse{
Status: status.NewInternal(ctx, "share or token not found"),
}, nil
}
}
// the share is minimally populated, we need more than the token
// look up complete share
share, _, st, err := s.resolveToken(ctx, share.Token)
switch {
case err != nil:
return nil, err
case st != nil:
return &provider.ListContainerResponse{
Status: st,
}, nil
}
if share.GetPermissions() == nil || !share.GetPermissions().Permissions.ListContainer {
if info.PermissionSet == nil || !info.PermissionSet.ListContainer {
return &provider.ListContainerResponse{
Status: status.NewPermissionDenied(ctx, nil, "share does not grant ListContainer permission"),
}, nil
@@ -852,7 +822,7 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer
ctx,
&provider.ListContainerRequest{
Ref: &provider.Reference{
ResourceId: share.ResourceId,
ResourceId: info.Id,
// prefix relative path with './' to make it a CS3 relative path
Path: utils.MakeRelativePath(req.Ref.Path),
},
@@ -867,7 +837,7 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer
for i := range listContainerR.Infos {
// FIXME how do we reduce permissions to what is granted by the public link?
// only a problem for id based access -> middleware
filterPermissions(listContainerR.Infos[i].PermissionSet, share.GetPermissions().Permissions)
filterPermissions(listContainerR.Infos[i].PermissionSet, info.PermissionSet)
if err := addShare(listContainerR.Infos[i], share); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", listContainerR.Infos[i]).Msg("error when adding share")
}
@@ -953,45 +923,70 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
}
// resolveToken returns the path and share for the publicly shared resource.
func (s *service) resolveToken(ctx context.Context, token string) (*link.PublicShare, *provider.ResourceInfo, *rpc.Status, error) {
driver, err := pool.GetGatewayServiceClient(s.conf.GatewayAddr)
if err != nil {
return nil, nil, nil, err
}
publicShareResponse, err := driver.GetPublicShare(
ctx,
&link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Token{
Token: token,
},
},
Sign: true,
},
)
switch {
case err != nil:
return nil, nil, nil, err
case publicShareResponse.Status.Code != rpc.Code_CODE_OK:
return nil, nil, publicShareResponse.Status, nil
}
// resolveToken returns the resource info for the publicly shared resource.
func (s *service) resolveToken(ctx context.Context, share interface{}) (*provider.ResourceInfo, interface{}, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
resourceID := &provider.ResourceId{}
perms := &provider.ResourcePermissions{}
var resolvedShare interface{}
switch v := share.(type) {
case *link.PublicShare:
publicShareResponse, err := gatewayClient.GetPublicShare(
ctx,
&link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Token{
Token: v.Token,
},
},
Sign: true,
},
)
switch {
case err != nil:
return nil, nil, err
case publicShareResponse.Status.Code != rpc.Code_CODE_OK:
return nil, nil, errtypes.NewErrtypeFromStatus(publicShareResponse.Status)
}
resolvedShare = publicShareResponse.GetShare()
resourceID = publicShareResponse.GetShare().GetResourceId()
perms = publicShareResponse.GetShare().GetPermissions().GetPermissions()
case *ocm.Share:
gsr, err := gatewayClient.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{
Token: v.Token,
})
switch {
case err != nil:
return nil, nil, err
case gsr.Status.Code != rpc.Code_CODE_OK:
return nil, nil, errtypes.NewErrtypeFromStatus(gsr.Status)
}
accessMethods := gsr.GetShare().GetAccessMethods()
if len(accessMethods) == 0 {
return nil, nil, errtypes.PermissionDenied("failed to get access to the requested resource")
}
resolvedShare = gsr.GetShare()
resourceID = gsr.GetShare().GetResourceId()
perms = accessMethods[0].GetWebdavOptions().Permissions
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: publicShareResponse.GetShare().GetResourceId(),
ResourceId: resourceID,
},
})
switch {
case err != nil:
return nil, nil, nil, err
return nil, nil, err
case sRes.Status.Code != rpc.Code_CODE_OK:
return nil, nil, sRes.Status, nil
return nil, nil, errtypes.NewErrtypeFromStatus(sRes.Status)
}
return publicShareResponse.GetShare(), sRes.Info, nil, nil
// Set permissions
sRes.Info.PermissionSet = perms
return sRes.Info, resolvedShare, nil
}
@@ -246,7 +246,10 @@ func (s *service) ListReceivedShares(ctx context.Context, req *collaboration.Lis
if !foundExclude {
req.Filters = append(req.Filters, &collaboration.Filter{Type: collaboration.Filter_TYPE_EXCLUDE_DENIALS})
}
shares, err := s.sm.ListReceivedShares(ctx, req.Filters) // TODO(labkode): check what to update
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
shares, err := s.sm.ListReceivedShares(ctx, req.Filters, &uid) // TODO(labkode): check what to update
if err != nil {
return &collaboration.ListReceivedSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
+138 -80
View File
@@ -27,6 +27,7 @@ import (
"time"
"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -69,7 +70,7 @@ type config struct {
CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"`
CredentialChain []string `mapstructure:"credential_chain"`
CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"`
TokenStrategy string `mapstructure:"token_strategy"`
TokenStrategyChain []string `mapstructure:"token_strategy_chain"`
TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"`
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
@@ -97,8 +98,8 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider
conf.GatewaySvc = sharedconf.GetGatewaySVC(conf.GatewaySvc)
// set defaults
if conf.TokenStrategy == "" {
conf.TokenStrategy = "header"
if len(conf.TokenStrategyChain) == 0 {
conf.TokenStrategyChain = []string{"header"}
}
if conf.TokenWriter == "" {
@@ -109,10 +110,6 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider
conf.TokenManager = "jwt"
}
if len(conf.CredentialChain) == 0 {
conf.CredentialChain = []string{"basic", "bearer"}
}
if conf.CredentialsByUserAgent == nil {
conf.CredentialsByUserAgent = map[string]string{}
}
@@ -139,19 +136,22 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider
credChain[key] = credStrategy
}
g, ok := tokenregistry.NewTokenFuncs[conf.TokenStrategy]
if !ok {
return nil, fmt.Errorf("token strategy not found: %s", conf.TokenStrategy)
}
tokenStrategy, err := g(conf.TokenStrategies[conf.TokenStrategy])
if err != nil {
return nil, err
tokenStrategyChain := make([]auth.TokenStrategy, 0, len(conf.TokenStrategyChain))
for _, strategy := range conf.TokenStrategyChain {
g, ok := tokenregistry.NewTokenFuncs[strategy]
if !ok {
return nil, fmt.Errorf("token strategy not found: %s", strategy)
}
tokenStrategy, err := g(conf.TokenStrategies[strategy])
if err != nil {
return nil, err
}
tokenStrategyChain = append(tokenStrategyChain, tokenStrategy)
}
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, fmt.Errorf("token manager not found: %s", conf.TokenStrategy)
return nil, fmt.Errorf("token manager not found: %s", conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
@@ -196,7 +196,7 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider
isUnprotectedEndpoint = true
}
ctx, err := authenticateUser(w, r, conf, tokenStrategy, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint)
ctx, err := authenticateUser(w, r, conf, tokenStrategyChain, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint)
if err != nil {
if !isUnprotectedEndpoint {
return
@@ -216,7 +216,7 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider
return chain, nil
}
func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategy auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) {
func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategies []auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
@@ -229,71 +229,85 @@ func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, toke
return nil, err
}
tkn := tokenStrategy.GetToken(r)
if tkn == "" {
log.Warn().Msg("core access token not set")
userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain)
// obtain credentials (basic auth, bearer token, ...) based on user agent
var creds *auth.Credentials
for _, k := range userAgentCredKeys {
creds, err = credChain[k].GetCredentials(w, r)
if err != nil {
log.Debug().Err(err).Msg("error retrieving credentials")
}
if creds != nil {
log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID)
break
}
}
// if no credentials are found, reply with authentication challenge depending on user agent
if creds == nil {
if !isUnprotectedEndpoint {
for _, key := range userAgentCredKeys {
if cred, ok := credChain[key]; ok {
cred.AddWWWAuthenticate(w, r, conf.Realm)
} else {
panic("auth credential strategy: " + key + "must have been loaded in init method")
}
// reva token or auth token can be passed using the same technique (for example bearer)
// before validating it against an auth provider, we can check directly if it's a reva
// token and if not try to use it for authenticating the user.
for _, tokenStrategy := range tokenStrategies {
token := tokenStrategy.GetToken(r)
if token != "" {
if user, tokenScope, ok := isTokenValid(r, tokenManager, token); ok {
if err := insertGroupsInUser(ctx, userGroupsCache, client, user); err != nil {
logError(isUnprotectedEndpoint, log, err, "got an error retrieving groups for user "+user.Username, http.StatusInternalServerError, w)
return nil, err
}
w.WriteHeader(http.StatusUnauthorized)
return ctxWithUserInfo(ctx, r, user, token, tokenScope), nil
}
return nil, errtypes.PermissionDenied("no credentials found")
}
req := &gateway.AuthenticateRequest{
Type: creds.Type,
ClientId: creds.ClientID,
ClientSecret: creds.ClientSecret,
}
log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc)
res, err := client.Authenticate(ctx, req)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w)
return nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
err := status.NewErrorFromCode(res.Status.Code, "auth")
logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w)
return nil, err
}
log.Info().Msg("core access token generated")
// write token to response
tkn = res.Token
tokenWriter.WriteToken(tkn, w)
} else {
log.Debug().Msg("access token is already provided")
}
log.Warn().Msg("core access token not set")
userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain)
// obtain credentials (basic auth, bearer token, ...) based on user agent
var creds *auth.Credentials
for _, k := range userAgentCredKeys {
creds, err = credChain[k].GetCredentials(w, r)
if err != nil {
log.Debug().Err(err).Msg("error retrieving credentials")
}
if creds != nil {
log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID)
break
}
}
// if no credentials are found, reply with authentication challenge depending on user agent
if creds == nil {
if !isUnprotectedEndpoint {
for _, key := range userAgentCredKeys {
if cred, ok := credChain[key]; ok {
cred.AddWWWAuthenticate(w, r, conf.Realm)
} else {
log.Error().Msg("auth credential strategy: " + key + "must have been loaded in init method")
w.WriteHeader(http.StatusInternalServerError)
return nil, errtypes.InternalError("no credentials found")
}
}
w.WriteHeader(http.StatusUnauthorized)
}
return nil, errtypes.PermissionDenied("no credentials found")
}
req := &gateway.AuthenticateRequest{
Type: creds.Type,
ClientId: creds.ClientID,
ClientSecret: creds.ClientSecret,
}
log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc)
res, err := client.Authenticate(ctx, req)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w)
return nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
err := status.NewErrorFromCode(res.Status.Code, "auth")
logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w)
return nil, err
}
log.Info().Msg("core access token generated") // write token to response
// write token to response
token := res.Token
tokenWriter.WriteToken(token, w)
// validate token
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), tkn)
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), token)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error dismantling token", http.StatusUnauthorized, w)
return nil, err
@@ -329,15 +343,59 @@ func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, toke
// store user and core access token in context.
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials?
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials?
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent())
// store scopes in context
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
return ctx, nil
return ctxWithUserInfo(ctx, r, u, token, tokenScope), nil
}
func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, token string, tokenScope map[string]*authpb.Scope) context.Context {
ctx = ctxpkg.ContextSetUser(ctx, user)
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent())
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
return ctx
}
func insertGroupsInUser(ctx context.Context, userGroupsCache gcache.Cache, client gateway.GatewayAPIClient, user *userpb.User) error {
if sharedconf.SkipUserGroupsInToken() {
var groups []string
if groupsIf, err := userGroupsCache.Get(user.Id.OpaqueId); err == nil {
groups = groupsIf.([]string)
} else {
groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: user.Id})
if err != nil {
return err
}
groups = groupsRes.Groups
_ = userGroupsCache.SetWithExpire(user.Id.OpaqueId, groupsRes.Groups, 3600*time.Second)
}
user.Groups = groups
}
return nil
}
func isTokenValid(r *http.Request, tokenManager token.Manager, token string) (*userpb.User, map[string]*authpb.Scope, bool) {
ctx := r.Context()
u, tokenScope, err := tokenManager.DismantleToken(ctx, token)
if err != nil {
return nil, nil, false
}
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path)
if err != nil {
return nil, nil, false
}
return u, tokenScope, ok
}
func logError(isUnprotectedEndpoint bool, log *zerolog.Logger, err error, msg string, status int, w http.ResponseWriter) {
@@ -22,5 +22,6 @@ import (
// Load core authentication strategies.
_ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/basic"
_ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/bearer"
_ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/ocmshares"
// Add your own here.
)
@@ -0,0 +1,58 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmshares
import (
"fmt"
"net/http"
"github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/cs3org/reva/v2/pkg/auth"
)
func init() {
registry.Register("ocmshares", New)
}
const (
headerShareToken = "ocm-token"
)
type strategy struct{}
// New returns a new auth strategy that handles public share verification.
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
token := r.Header.Get(headerShareToken)
if token == "" {
token = r.URL.Query().Get(headerShareToken)
}
if token == "" {
return nil, fmt.Errorf("no ocm token provided")
}
return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
}
@@ -20,6 +20,7 @@ package loader
import (
// Load core token strategies.
_ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/strategy/bearer"
_ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/strategy/header"
// Add your own here.
)
@@ -0,0 +1,84 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package header
import (
"mime"
"net/http"
"strings"
"github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/registry"
"github.com/cs3org/reva/v2/pkg/auth"
)
func init() {
registry.Register("bearer", New)
}
type b struct{}
// New returns a new auth strategy that checks for bearer auth.
func New(m map[string]interface{}) (auth.TokenStrategy, error) {
return b{}, nil
}
func (b) GetToken(r *http.Request) string {
// Authorization Request Header Field: https://www.rfc-editor.org/rfc/rfc6750#section-2.1
if tkn, ok := getFromAuthorizationHeader(r); ok {
return tkn
}
// Form-Encoded Body Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.2
if tkn, ok := getFromBody(r); ok {
return tkn
}
// URI Query Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.3
if tkn, ok := getFromQueryParam(r); ok {
return tkn
}
return ""
}
func getFromAuthorizationHeader(r *http.Request) (string, bool) {
auth := r.Header.Get("Authorization")
tkn := strings.TrimPrefix(auth, "Bearer ")
return tkn, tkn != ""
}
func getFromBody(r *http.Request) (string, bool) {
mediatype, _, err := mime.ParseMediaType(r.Header.Get("content-type"))
if err != nil {
return "", false
}
if mediatype != "application/x-www-form-urlencoded" {
return "", false
}
if err = r.ParseForm(); err != nil {
return "", false
}
tkn := r.Form.Get("access-token")
return tkn, tkn != ""
}
func getFromQueryParam(r *http.Request) (string, bool) {
tkn := r.URL.Query().Get("access_token")
return tkn, tkn != ""
}
@@ -44,7 +44,7 @@ import (
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/cs3org/reva/pkg/storage/utils/decomposedfs/tree")
tracer = otel.Tracer("github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/tree")
}
const (
@@ -324,6 +324,7 @@ func (s *svc) doPut(w http.ResponseWriter, r *http.Request) {
return
}
httpReq.Header = r.Header
httpReq.ContentLength = r.ContentLength
httpRes, err := httpClient.Do(httpReq)
if err != nil {
+1 -1
View File
@@ -26,7 +26,6 @@ import (
_ "github.com/cs3org/reva/v2/internal/http/services/dataprovider"
_ "github.com/cs3org/reva/v2/internal/http/services/helloworld"
_ "github.com/cs3org/reva/v2/internal/http/services/mentix"
_ "github.com/cs3org/reva/v2/internal/http/services/meshdirectory"
_ "github.com/cs3org/reva/v2/internal/http/services/metrics"
_ "github.com/cs3org/reva/v2/internal/http/services/ocmd"
_ "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav"
@@ -34,6 +33,7 @@ import (
_ "github.com/cs3org/reva/v2/internal/http/services/preferences"
_ "github.com/cs3org/reva/v2/internal/http/services/prometheus"
_ "github.com/cs3org/reva/v2/internal/http/services/reverseproxy"
_ "github.com/cs3org/reva/v2/internal/http/services/sciencemesh"
_ "github.com/cs3org/reva/v2/internal/http/services/siteacc"
_ "github.com/cs3org/reva/v2/internal/http/services/sysinfo"
_ "github.com/cs3org/reva/v2/internal/http/services/wellknown"
@@ -1,155 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package meshdirectory
import (
"encoding/json"
"fmt"
"net/http"
meshdirectoryweb "github.com/sciencemesh/meshdirectory-web"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/ocmd"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cs3org/reva/v2/pkg/rhttp/global"
"github.com/mitchellh/mapstructure"
)
func init() {
global.Register("meshdirectory", New)
}
type config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
func (c *config) init() {
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
if c.Prefix == "" {
c.Prefix = "meshdir"
}
}
type svc struct {
conf *config
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns a new Mesh Directory HTTP service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
service := &svc{
conf: c,
}
return service, nil
}
// Service prefix
func (s *svc) Prefix() string {
return s.conf.Prefix
}
// Unprotected endpoints
func (s *svc) Unprotected() []string {
return []string{"/"}
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) getClient() (gateway.GatewayAPIClient, error) {
return pool.GetGatewayServiceClient(s.conf.GatewaySvc)
}
func (s *svc) serveJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
gatewayClient, err := s.getClient()
if err != nil {
ocmd.WriteError(w, r, ocmd.APIErrorServerError,
fmt.Sprintf("error getting grpc client on addr: %v", s.conf.GatewaySvc), err)
return
}
providers, err := gatewayClient.ListAllProviders(ctx, &providerv1beta1.ListAllProvidersRequest{})
if err != nil {
ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error listing all providers", err)
return
}
jsonResponse, err := json.Marshal(providers.Providers)
if err != nil {
ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error marshalling providers data", err)
return
}
// Write response
_, err = w.Write(jsonResponse)
if err != nil {
ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error writing providers data", err)
return
}
w.WriteHeader(http.StatusOK)
}
// HTTP service handler
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
switch head {
case "providers":
s.serveJSON(w, r)
return
default:
r.URL.Path = head + r.URL.Path
meshdirectoryweb.ServeMeshDirectorySPA(w, r)
return
}
})
}
-90
View File
@@ -1,90 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
import (
"encoding/json"
"fmt"
"net/http"
"github.com/cs3org/reva/v2/pkg/appctx"
)
type configData struct {
Enabled bool `json:"enabled" xml:"enabled"`
APIVersion string `json:"apiVersion" xml:"apiVersion"`
Host string `json:"host" xml:"host"`
Endpoint string `json:"endPoint" xml:"endPoint"`
Provider string `json:"provider" xml:"provider"`
ResourceTypes []resourceTypes `json:"resourceTypes" xml:"resourceTypes"`
}
type resourceTypes struct {
Name string `json:"name"`
ShareTypes []string `json:"shareTypes"`
Protocols resourceTypesProtocols `json:"protocols"`
}
type resourceTypesProtocols struct {
Webdav string `json:"webdav"`
}
type configHandler struct {
c configData
}
func (h *configHandler) init(c *Config) {
h.c = c.Config
if h.c.APIVersion == "" {
h.c.APIVersion = "1.0-proposal1"
}
if h.c.Host == "" {
h.c.Host = "localhost"
}
if h.c.Provider == "" {
h.c.Provider = "cernbox"
}
h.c.Enabled = true
if len(c.Prefix) > 0 {
h.c.Endpoint = fmt.Sprintf("https://%s/%s", h.c.Host, c.Prefix)
} else {
h.c.Endpoint = fmt.Sprintf("https://%s", h.c.Host)
}
h.c.ResourceTypes = []resourceTypes{{
Name: "file",
ShareTypes: []string{"user"},
Protocols: resourceTypesProtocols{
Webdav: fmt.Sprintf("/%s/ocm_webdav", h.c.Provider),
},
}}
}
func (h *configHandler) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
indentedConf, _ := json.MarshalIndent(h.c, "", " ")
if _, err := w.Write(indentedConf); err != nil {
log.Err(err).Msg("Error writing to ResponseWriter")
}
})
}
+82 -247
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -22,323 +22,158 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/smtpclient"
"github.com/cs3org/reva/v2/pkg/utils"
)
type invitesHandler struct {
smtpCredentials *smtpclient.SMTPCredentials
gatewayAddr string
meshDirectoryURL string
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (h *invitesHandler) init(c *Config) {
h.gatewayAddr = c.GatewaySvc
if c.SMTPCredentials != nil {
h.smtpCredentials = smtpclient.NewSMTPCredentials(c.SMTPCredentials)
func (h *invitesHandler) init(c *config) error {
var err error
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.meshDirectoryURL = c.MeshDirectoryURL
h.gatewaySelector = gatewaySelector
return nil
}
func (h *invitesHandler) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
switch head {
case "":
h.generateInviteToken(w, r)
case "forward":
h.forwardInvite(w, r)
case "accept":
h.acceptInvite(w, r)
case "find-accepted-users":
h.findAcceptedUsers(w, r)
case "generate":
h.generate(w, r)
default:
w.WriteHeader(http.StatusNotFound)
}
})
type acceptInviteRequest struct {
Token string `json:"token"`
UserID string `json:"userID"`
RecipientProvider string `json:"recipientProvider"`
Name string `json:"name"`
Email string `json:"email"`
}
func (h *invitesHandler) generateInviteToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err)
return
}
token, err := gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{})
if err != nil {
WriteError(w, r, APIErrorServerError, "error generating token", err)
return
}
if r.FormValue("recipient") != "" && h.smtpCredentials != nil {
usr := ctxpkg.ContextMustGetUser(ctx)
// TODO: the message body needs to point to the meshdirectory service
subject := fmt.Sprintf("ScienceMesh: %s wants to collaborate with you", usr.DisplayName)
body := "Hi,\n\n" +
usr.DisplayName + " (" + usr.Mail + ") wants to start sharing OCM resources with you. " +
"To accept the invite, please visit the following URL:\n" +
h.meshDirectoryURL + "?token=" + token.InviteToken.Token + "&providerDomain=" + usr.Id.Idp + "\n\n" +
"Alternatively, you can visit your mesh provider and use the following details:\n" +
"Token: " + token.InviteToken.Token + "\n" +
"ProviderDomain: " + usr.Id.Idp + "\n\n" +
"Best,\nThe ScienceMesh team"
err = h.smtpCredentials.SendMail(r.FormValue("recipient"), subject, body)
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending token by mail", err)
return
}
}
jsonResponse, err := json.Marshal(token.InviteToken)
if err != nil {
WriteError(w, r, APIErrorServerError, "error marshalling token data", err)
return
}
// Write response
_, err = w.Write(jsonResponse)
if err != nil {
WriteError(w, r, APIErrorServerError, "error writing token data", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (h *invitesHandler) forwardInvite(w http.ResponseWriter, r *http.Request) {
// AcceptInvite informs avout an accepted invitation so that the users
// can initiate the OCM share creation.
func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
var token, providerDomain string
if err == nil && contentType == "application/json" {
defer r.Body.Close()
reqBody, err := io.ReadAll(r.Body)
if err == nil {
reqMap := make(map[string]string)
err = json.Unmarshal(reqBody, &reqMap)
if err == nil {
token, providerDomain = reqMap["token"], reqMap["providerDomain"]
}
}
} else {
token, providerDomain = r.FormValue("token"), r.FormValue("providerDomain")
}
if token == "" || providerDomain == "" {
WriteError(w, r, APIErrorInvalidParameter, "token and providerDomain must not be null", nil)
return
}
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
req, err := getAcceptInviteRequest(r)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err)
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
return
}
inviteToken := &invitepb.InviteToken{
Token: token,
}
providerInfo, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: providerDomain,
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc get invite by domain info request", err)
return
}
if providerInfo.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(providerInfo.Status.Message))
return
}
forwardInviteReq := &invitepb.ForwardInviteRequest{
InviteToken: inviteToken,
OriginSystemProvider: providerInfo.ProviderInfo,
}
forwardInviteResponse, err := gatewayClient.ForwardInvite(ctx, forwardInviteReq)
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc forward invite request", err)
return
}
if forwardInviteResponse.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(forwardInviteResponse.Status.Message))
return
}
_, err = w.Write([]byte("Accepted invite from: " + providerDomain))
if err != nil {
WriteError(w, r, APIErrorServerError, "error writing token data", err)
return
}
w.WriteHeader(http.StatusOK)
log.Info().Msgf("Invite forwarded to: %s", providerDomain)
}
func (h *invitesHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
var token, userID, recipientProvider, name, email string
if err == nil && contentType == "application/json" {
defer r.Body.Close()
reqBody, err := io.ReadAll(r.Body)
if err == nil {
reqMap := make(map[string]string)
err = json.Unmarshal(reqBody, &reqMap)
if err == nil {
token, userID, recipientProvider = reqMap["token"], reqMap["userID"], reqMap["recipientProvider"]
name, email = reqMap["name"], reqMap["email"]
}
}
} else {
token, userID, recipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider")
name, email = r.FormValue("name"), r.FormValue("email")
}
if token == "" || userID == "" || recipientProvider == "" {
WriteError(w, r, APIErrorInvalidParameter, "missing parameters in request", nil)
return
}
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err)
if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil)
return
}
clientIP, err := utils.GetClientIP(r)
if err != nil {
WriteError(w, r, APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
return
}
recipientProviderURL, err := url.Parse(recipientProvider)
if err != nil {
WriteError(w, r, APIErrorServerError, fmt.Sprintf("error parseing recipientProvider URL: %s", recipientProvider), err)
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
return
}
providerInfo := ocmprovider.ProviderInfo{
Domain: recipientProviderURL.Hostname(),
Domain: req.RecipientProvider,
Services: []*ocmprovider.Service{
{
Host: clientIP,
},
},
}
gatewayClient, err := h.gatewaySelector.Next()
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
return
}
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
Provider: &providerInfo,
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc is provider allowed request", err)
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
return
}
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message))
reqres.WriteError(w, r, reqres.APIErrorUntrustedService, "provider not trusted", errors.New(providerAllowedResp.Status.Message))
return
}
userObj := &userpb.User{
Id: &userpb.UserId{
OpaqueId: userID,
Idp: recipientProvider,
Type: userpb.UserType_USER_TYPE_PRIMARY,
OpaqueId: req.UserID,
Idp: req.RecipientProvider,
Type: userpb.UserType_USER_TYPE_FEDERATED,
},
Mail: email,
DisplayName: name,
Mail: req.Email,
DisplayName: req.Name,
}
acceptInviteRequest := &invitepb.AcceptInviteRequest{
InviteToken: &invitepb.InviteToken{
Token: token,
Token: req.Token,
},
RemoteUser: userObj,
}
acceptInviteResponse, err := gatewayClient.AcceptInvite(ctx, acceptInviteRequest)
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc accept invite request", err)
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc accept invite request", err)
return
}
if acceptInviteResponse.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorServerError, "grpc accept invite request failed", errors.New(acceptInviteResponse.Status.Message))
return
switch acceptInviteResponse.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil)
return
case rpc.Code_CODE_INVALID_ARGUMENT:
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil)
return
case rpc.Code_CODE_ALREADY_EXISTS:
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil)
return
default:
reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+acceptInviteResponse.Status.Message, errors.New(acceptInviteResponse.Status.Message))
return
}
}
log.Info().Msgf("User: %+v added to accepted users.", userObj)
}
func (h *invitesHandler) findAcceptedUsers(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
ctx := r.Context()
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err)
if err := json.NewEncoder(w).Encode(&user{
UserID: acceptInviteResponse.UserId.OpaqueId,
Email: acceptInviteResponse.Email,
Name: acceptInviteResponse.DisplayName,
}); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response", err)
return
}
response, err := gatewayClient.FindAcceptedUsers(ctx, &invitepb.FindAcceptedUsersRequest{
Filter: "",
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc find accepted users request", err)
return
}
indentedResponse, _ := json.MarshalIndent(response, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(indentedResponse); err != nil {
log.Err(err).Msg("Error writing to ResponseWriter")
}
}
func (h *invitesHandler) generate(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
ctx := r.Context()
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err)
return
}
response, err := gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc generate invite token request", err)
return
}
indentedResponse, _ := json.MarshalIndent(response, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(indentedResponse); err != nil {
log.Err(err).Msg("Error writing to ResponseWriter")
log.Info().Str("user", fmt.Sprintf("%s@%s", userObj.Id.OpaqueId, userObj.Id.Idp)).Str("token", req.Token).Msg("added to accepted users")
}
type user struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}
func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) {
var req acceptInviteRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
req.Token, req.UserID, req.RecipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider")
req.Name, req.Email = r.FormValue("name"), r.FormValue("email")
}
return &req, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,27 +19,48 @@
package ocmd
import (
"io"
"mime"
"net/http"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
)
type notificationsHandler struct {
// var validate = validator.New()
type notifHandler struct {
}
func (h *notificationsHandler) init(c *Config) {
func (h *notifHandler) init(c *config) error {
return nil
}
func (h *notificationsHandler) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
// Notifications dispatches any notifications received from remote OCM sites
// according to the specifications at:
// https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1notifications/post
func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
req, err := getNotification(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
// TODO(lopresti) this is all to be implemented. For now we just log what we got
log.Debug().Msgf("Received OCM notification: %+v", req)
log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
// this is to please Nextcloud
w.WriteHeader(http.StatusCreated)
}
w.WriteHeader(http.StatusOK)
})
func getNotification(r *http.Request) (string, error) {
// var req notificationRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
bytes, _ := io.ReadAll(r.Body)
return string(bytes), nil
}
return "", nil
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
import (
"net/http"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rhttp/global"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
)
func init() {
global.Register("ocmd", New)
}
type config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"`
}
func (c *config) ApplyDefaults() {
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
if c.Prefix == "" {
c.Prefix = "ocm"
}
}
type svc struct {
Conf *config
router chi.Router
}
// New returns a new ocmd object, that implements
// the OCM APIs specified in https://cs3org.github.io/OCM-API/docs.html
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
r := chi.NewRouter()
s := &svc{
Conf: &c,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
return s, nil
}
func (s *svc) routerInit() error {
sharesHandler := new(sharesHandler)
invitesHandler := new(invitesHandler)
notifHandler := new(notifHandler)
if err := sharesHandler.init(s.Conf); err != nil {
return err
}
if err := invitesHandler.init(s.Conf); err != nil {
return err
}
if err := notifHandler.init(s.Conf); err != nil {
return err
}
s.router.Post("/shares", sharesHandler.CreateShare)
s.router.Post("/invite-accepted", invitesHandler.AcceptInvite)
s.router.Post("/notifications", notifHandler.Notifications)
return nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.Conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/invite-accepted", "/shares", "/notifications"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Str("path", r.URL.Path).Msg("ocm routing")
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
r.URL.RawPath = ""
s.router.ServeHTTP(w, r)
})
}
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
import (
"net/http"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rhttp/global"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/smtpclient"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
)
func init() {
global.Register("ocmd", New)
}
// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"`
Prefix string `mapstructure:"prefix"`
Host string `mapstructure:"host"`
GatewaySvc string `mapstructure:"gatewaysvc"`
MeshDirectoryURL string `mapstructure:"mesh_directory_url"`
Config configData `mapstructure:"config"`
}
func (c *Config) init() {
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
// if c.Prefix == "" {
// c.Prefix = "ocm"
// }
}
type svc struct {
Conf *Config
SharesHandler *sharesHandler
NotificationsHandler *notificationsHandler
ConfigHandler *configHandler
InvitesHandler *invitesHandler
SendHandler *sendHandler
}
// New returns a new ocmd object
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
s := &svc{
Conf: conf,
}
s.SharesHandler = new(sharesHandler)
s.NotificationsHandler = new(notificationsHandler)
s.ConfigHandler = new(configHandler)
s.InvitesHandler = new(invitesHandler)
s.SendHandler = new(sendHandler)
s.SharesHandler.init(s.Conf)
s.NotificationsHandler.init(s.Conf)
log.Debug().Str("initializing ConfigHandler Host", s.Conf.Host)
s.ConfigHandler.init(s.Conf)
s.InvitesHandler.init(s.Conf)
s.SendHandler.init(s.Conf)
return s, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.Conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/invites/accept", "/shares", "/ocm-provider", "/notifications"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
switch head {
case "ocm-provider":
s.ConfigHandler.Handler().ServeHTTP(w, r)
return
case "shares":
s.SharesHandler.Handler().ServeHTTP(w, r)
return
case "notifications":
s.NotificationsHandler.Handler().ServeHTTP(w, r)
return
case "invites":
s.InvitesHandler.Handler().ServeHTTP(w, r)
return
case "send":
s.SendHandler.Handler().ServeHTTP(w, r)
}
log.Warn().Msg("request not handled")
w.WriteHeader(http.StatusNotFound)
})
}
@@ -0,0 +1,160 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ocmshare "github.com/cs3org/reva/v2/pkg/ocm/share"
utils "github.com/cs3org/reva/v2/pkg/utils"
)
// Protocols is the list of protocols.
type Protocols []Protocol
// Protocol represents the way of access the resource
// in the OCM share.
type Protocol interface {
// ToOCMProtocol convert the protocol to a ocm Protocol struct
ToOCMProtocol() *ocm.Protocol
}
// protocols supported by the OCM API
// WebDAV contains the parameters for the WebDAV protocol.
type WebDAV struct {
SharedSecret string `json:"sharedSecret" validate:"required"`
Permissions []string `json:"permissions" validate:"required,dive,required,oneof=read write share"`
URL string `json:"url" validate:"required"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *WebDAV) ToOCMProtocol() *ocm.Protocol {
perms := &ocm.SharePermissions{
Permissions: &providerv1beta1.ResourcePermissions{},
}
for _, p := range w.Permissions {
switch p {
case "read":
perms.Permissions.GetPath = true
perms.Permissions.InitiateFileDownload = true
perms.Permissions.ListContainer = true
perms.Permissions.Stat = true
case "write":
perms.Permissions.InitiateFileUpload = true
case "share":
perms.Reshare = true
}
}
return ocmshare.NewWebDAVProtocol(w.URL, w.SharedSecret, perms)
}
// Webapp contains the parameters for the Webapp protocol.
type Webapp struct {
URITemplate string `json:"uriTemplate" validate:"required"`
ViewMode string `json:"viewMode" validate:"required,dive,required,oneof=view read write"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *Webapp) ToOCMProtocol() *ocm.Protocol {
return ocmshare.NewWebappProtocol(w.URITemplate, utils.GetAppViewMode(w.ViewMode))
}
// Datatx contains the parameters for the Datatx protocol.
type Datatx struct {
SharedSecret string `json:"sharedSecret" validate:"required"`
SourceURI string `json:"srcUri" validate:"required"`
Size uint64 `json:"size" validate:"required"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *Datatx) ToOCMProtocol() *ocm.Protocol {
return ocmshare.NewTransferProtocol(w.SourceURI, w.SharedSecret, w.Size)
}
var protocolImpl = map[string]reflect.Type{
"webdav": reflect.TypeOf(WebDAV{}),
"webapp": reflect.TypeOf(Webapp{}),
"datatx": reflect.TypeOf(Datatx{}),
}
// UnmarshalJSON implements the Unmarshaler interface.
func (p *Protocols) UnmarshalJSON(data []byte) error {
var prot map[string]json.RawMessage
if err := json.Unmarshal(data, &prot); err != nil {
return err
}
*p = []Protocol{}
for name, d := range prot {
var res Protocol
// we do not support the OCM v1.0 properties for now, therefore just skip or bail out
if name == "name" {
continue
}
if name == "options" {
var opt map[string]any
if err := json.Unmarshal(d, &opt); err != nil || len(opt) > 0 {
return fmt.Errorf("protocol options not supported: %s", string(d))
}
continue
}
ctype, ok := protocolImpl[name]
if !ok {
return fmt.Errorf("protocol %s not recognised", name)
}
res = reflect.New(ctype).Interface().(Protocol)
if err := json.Unmarshal(d, &res); err != nil {
return err
}
*p = append(*p, res)
}
return nil
}
// MarshalJSON implements the Marshaler interface.
func (p Protocols) MarshalJSON() ([]byte, error) {
if len(p) == 0 {
return nil, errors.New("no protocol defined")
}
d := make(map[string]any)
for _, prot := range p {
d[getProtocolName(prot)] = prot
}
// fill in the OCM v1.0 properties
d["name"] = "multi"
d["options"] = map[string]any{}
return json.Marshal(d)
}
func getProtocolName(p Protocol) string {
n := reflect.TypeOf(p).String()
s := strings.Split(n, ".")
return strings.ToLower(s[len(s)-1])
}
-200
View File
@@ -1,200 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"google.golang.org/grpc/metadata"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
)
type sendHandler struct {
GatewaySvc string
}
func (h *sendHandler) init(c *Config) {
h.GatewaySvc = c.GatewaySvc
}
func (h *sendHandler) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
defer r.Body.Close()
reqBody, err := io.ReadAll(r.Body)
if err != nil {
log.Error().Msg("cannot read POST body!")
w.WriteHeader(http.StatusInternalServerError)
return
}
reqMap := make(map[string]string)
err = json.Unmarshal(reqBody, &reqMap)
if err != nil {
log.Error().Msg("cannot parse POST body!")
w.WriteHeader(http.StatusInternalServerError)
return
}
sourcePath, targetPath, sharedSecret := reqMap["sourcePath"], reqMap["targetPath"], reqMap["sharedSecret"]
recipientUsername, recipientHost := reqMap["recipientUsername"], reqMap["recipientHost"]
loginType, loginUsername, loginPassword := reqMap["loginType"], reqMap["loginUsername"], reqMap["loginPassword"]
// "sourcePath": "other",
// "targetPath": "sciencemesh\/other",
// "type": "dir", (unused)
// "recipientUsername": "marie",
// "recipientHost": "revanc2.docker",
// "loginType": "basic",
// "loginUsername": "einstein",
// "loginPassword": "Ny4Nv6WLoC1o70kVgrVOZLZ2vRgPjuej"
gatewayAddr := h.GatewaySvc
gatewayClient, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
log.Error().Msg("cannot get grpc client!")
w.WriteHeader(http.StatusInternalServerError)
return
}
loginReq := &gateway.AuthenticateRequest{
Type: loginType,
ClientId: loginUsername,
ClientSecret: loginPassword,
}
loginCtx := context.Background()
res, err := gatewayClient.Authenticate(loginCtx, loginReq)
if err != nil {
log.Error().Msg("error logging in")
w.WriteHeader(http.StatusInternalServerError)
return
}
authCtx := context.Background()
authCtx = ctxpkg.ContextSetToken(authCtx, res.Token)
authCtx = metadata.AppendToOutgoingContext(authCtx, ctxpkg.TokenHeader, res.Token)
// copied from cmd/reva/public-share-create.go:
ref := &provider.Reference{Path: sourcePath}
req := &provider.StatRequest{Ref: ref}
res2, err := gatewayClient.Stat(authCtx, req)
if err != nil {
log.Error().Msg("error sending: stat file/folder to share")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res2.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("error returned: stat file/folder to share")
w.WriteHeader(http.StatusInternalServerError)
return
}
// see cmd/reva/share-creat.go:getSharePerm
readerPermission := &provider.ResourcePermissions{
GetPath: true,
InitiateFileDownload: true,
ListFileVersions: true,
ListContainer: true,
Stat: true,
}
grant := &ocm.ShareGrant{
Permissions: &ocm.SharePermissions{
Permissions: readerPermission,
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
Idp: recipientHost,
OpaqueId: recipientUsername,
},
},
},
}
recipientProviderInfo, err := gatewayClient.GetInfoByDomain(authCtx, &ocmprovider.GetInfoByDomainRequest{
Domain: recipientHost,
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc get invite by domain info request", err)
return
}
if recipientProviderInfo.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(recipientProviderInfo.Status.Message))
return
}
shareRequest := &ocm.CreateOCMShareRequest{
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"permissions": {
Decoder: "plain",
Value: []byte(strconv.Itoa(0)),
},
"name": {
Decoder: "plain",
Value: []byte(targetPath),
},
"protocol": {
Decoder: "plain",
Value: []byte("webdav"), // TODO: support datatx too
},
"token": {
Decoder: "plain",
Value: []byte(sharedSecret),
},
},
},
ResourceId: res2.Info.Id,
Grant: grant,
RecipientMeshProvider: recipientProviderInfo.ProviderInfo,
}
shareRes, err := gatewayClient.CreateOCMShare(authCtx, shareRequest)
if err != nil {
log.Error().Msg("error sending: CreateShare")
w.WriteHeader(http.StatusInternalServerError)
return
}
if shareRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("error returned: CreateShare")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
}
+159 -152
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -22,102 +22,83 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"mime"
"net/http"
"reflect"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-playground/validator/v10"
)
var validate = validator.New()
type sharesHandler struct {
gatewayAddr string
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
exposeRecipientDisplayName bool
}
func (h *sharesHandler) init(c *Config) {
h.gatewayAddr = c.GatewaySvc
func (h *sharesHandler) init(c *config) error {
var err error
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.gatewaySelector = gatewaySelector
h.exposeRecipientDisplayName = c.ExposeRecipientDisplayName
return nil
}
func (h *sharesHandler) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
h.createShare(w, r)
default:
WriteError(w, r, APIErrorInvalidParameter, "Only POST method is allowed", nil)
}
})
type createShareRequest struct {
ShareWith string `json:"shareWith" validate:"required"` // identifier of the recipient of the share
Name string `json:"name" validate:"required"` // name of the resource
Description string `json:"description"` // (optional) description of the resource
ProviderID string `json:"providerId" validate:"required"` // unique identifier of the resource at provider side
Owner string `json:"owner" validate:"required"` // unique identifier of the owner at provider side
Sender string `json:"sender" validate:"required"` // unique indentifier of the user who wants to share the resource at provider side
OwnerDisplayName string `json:"ownerDisplayName"` // display name of the owner of the resource
SenderDisplayName string `json:"senderDisplayName"` // dispay name of the user who wants to share the resource
ShareType string `json:"shareType" validate:"required,oneof=user group"` // recipient share type (user or group)
ResourceType string `json:"resourceType" validate:"required,oneof=file folder"`
Expiration uint64 `json:"expiration"`
Protocols Protocols `json:"protocol" validate:"required"`
}
func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) {
// CreateShare sends all the informations to the consumer needed to start
// synchronization between the two services.
func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
var shareWith, meshProvider, resource, providerID, owner string
var protocol map[string]interface{}
if err == nil && contentType == "application/json" {
defer r.Body.Close()
reqBody, err := io.ReadAll(r.Body)
if err == nil {
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBody, &reqMap)
if err == nil {
meshProvider = reqMap["meshProvider"].(string) // FIXME: get this from sharedBy string?
shareWith, protocol = reqMap["shareWith"].(string), reqMap["protocol"].(map[string]interface{})
resource, owner = reqMap["name"].(string), reqMap["owner"].(string)
// Note that if an OCM request were to go directly from a Nextcloud server
// to a Reva server, it will (incorrectly) sends an integer provider_id instead a string one.
// This doesn't happen when using the sciencemesh-nextcloud app, but in order to make the OCM
// test suite pass, this code works around that:
if reflect.ValueOf(reqMap["providerId"]).Kind() == reflect.Float64 {
providerID = fmt.Sprintf("%d", int(math.Round(reqMap["providerId"].(float64))))
} else {
providerID = reqMap["providerId"].(string)
}
} else {
WriteError(w, r, APIErrorInvalidParameter, "could not parse json request body", nil)
}
}
} else {
var protocolJSON string
shareWith, protocolJSON, meshProvider = r.FormValue("shareWith"), r.FormValue("protocol"), r.FormValue("meshProvider")
resource, providerID, owner = r.FormValue("name"), r.FormValue("providerId"), r.FormValue("owner")
err = json.Unmarshal([]byte(protocolJSON), &protocol)
if err != nil {
WriteError(w, r, APIErrorInvalidParameter, "invalid protocol parameters", nil)
}
}
if resource == "" || providerID == "" || owner == "" {
WriteError(w, r, APIErrorInvalidParameter, "missing details about resource to be shared", nil)
return
}
if shareWith == "" || protocol["name"] == "" || meshProvider == "" {
WriteError(w, r, APIErrorInvalidParameter, "missing request parameters", nil)
return
}
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
req, err := getCreateShareRequest(r)
if err != nil {
WriteError(w, r, APIErrorServerError, "error getting storage grpc client", err)
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
_, meshProvider, err := getIDAndMeshProvider(req.Sender)
log.Debug().Msgf("Determined Mesh Provider '%s' from req.Sender '%s'", meshProvider, req.Sender)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
clientIP, err := utils.GetClientIP(r)
if err != nil {
WriteError(w, r, APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
return
}
providerInfo := ocmprovider.ProviderInfo{
@@ -128,128 +109,154 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) {
},
},
}
gatewayClient, err := h.gatewaySelector.Next()
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
return
}
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
Provider: &providerInfo,
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc is provider allowed request", err)
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
return
}
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message))
reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message))
return
}
shareWith, _, err := getIDAndMeshProvider(req.ShareWith)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
shareWithParts := strings.Split(shareWith, "@")
userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{
UserId: &userpb.UserId{OpaqueId: shareWithParts[0]}, SkipFetchingUserGroups: true,
UserId: &userpb.UserId{OpaqueId: shareWith}, SkipFetchingUserGroups: true,
})
if err != nil {
WriteError(w, r, APIErrorServerError, "error searching recipient", err)
reqres.WriteError(w, r, reqres.APIErrorServerError, "error searching recipient", err)
return
}
if userRes.Status.Code != rpc.Code_CODE_OK {
WriteError(w, r, APIErrorNotFound, "user not found", errors.New(userRes.Status.Message))
reqres.WriteError(w, r, reqres.APIErrorNotFound, "user not found", errors.New(userRes.Status.Message))
return
}
var permissions conversions.Permissions
var token string
options, ok := protocol["options"].(map[string]interface{})
if !ok {
WriteError(w, r, APIErrorInvalidParameter, "protocol: webdav token not provided", nil)
return
}
token, ok = options["sharedSecret"].(string)
if !ok {
token, ok = options["token"].(string)
if !ok {
WriteError(w, r, APIErrorInvalidParameter, "protocol: webdav token not provided", nil)
return
}
}
var role *conversions.Role
pval, ok := options["permissions"].(int)
if !ok {
WriteError(w, r, APIErrorInvalidParameter, "permissions not provided", nil)
return
}
permissions, err = conversions.NewPermissions(pval)
owner, err := getUserIDFromOCMUser(req.Owner)
if err != nil {
WriteError(w, r, APIErrorInvalidParameter, err.Error(), nil)
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
role = conversions.RoleFromOCSPermissions(permissions)
val, err := json.Marshal(role.CS3ResourcePermissions())
sender, err := getUserIDFromOCMUser(req.Sender)
if err != nil {
WriteError(w, r, APIErrorServerError, "could not encode role", nil)
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
ownerID := &userpb.UserId{
OpaqueId: owner,
Idp: meshProvider,
Type: userpb.UserType_USER_TYPE_PRIMARY,
}
createShareReq := &ocmcore.CreateOCMCoreShareRequest{
Name: resource,
ProviderId: providerID,
Owner: ownerID,
ShareWith: userRes.User.GetId(),
Protocol: &ocmcore.Protocol{
Name: protocol["name"].(string),
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"permissions": {
Decoder: "json",
Value: val,
},
"token": {
Decoder: "plain",
Value: []byte(token),
},
},
},
},
Description: req.Description,
Name: req.Name,
ResourceId: req.ProviderID,
Owner: owner,
Sender: sender,
ShareWith: userRes.User.Id,
ResourceType: getResourceTypeFromOCMRequest(req.ResourceType),
ShareType: getOCMShareType(req.ShareType),
Protocols: getProtocols(req.Protocols),
}
createShareResponse, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq)
if err != nil {
WriteError(w, r, APIErrorServerError, "error sending a grpc create ocm core share request", err)
return
}
if createShareResponse.Status.Code != rpc.Code_CODE_OK {
if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND {
WriteError(w, r, APIErrorNotFound, "not found", nil)
return
if req.Expiration != 0 {
createShareReq.Expiration = &types.Timestamp{
Seconds: req.Expiration,
}
WriteError(w, r, APIErrorServerError, "grpc create ocm core share request failed", errors.New(createShareResponse.Status.Message))
return
}
timeCreated := createShareResponse.Created
jsonOut, err := json.Marshal(
map[string]string{
"id": createShareResponse.Id,
"createdAt": time.Unix(int64(timeCreated.Seconds), int64(timeCreated.Nanos)).String(),
},
)
createShareResp, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq)
if err != nil {
WriteError(w, r, APIErrorServerError, "error marshalling share data", err)
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", err)
return
}
if userRes.Status.Code != rpc.Code_CODE_OK {
// TODO: define errors in the cs3apis
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message))
return
}
response := map[string]any{}
if h.exposeRecipientDisplayName {
response["recipientDisplayName"] = userRes.User.DisplayName
}
_ = json.NewEncoder(w).Encode(response)
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
}
_, err = w.Write(jsonOut)
func getUserIDFromOCMUser(user string) (*userpb.UserId, error) {
id, idp, err := getIDAndMeshProvider(user)
if err != nil {
WriteError(w, r, APIErrorServerError, "error writing shares data", err)
return
return nil, err
}
return &userpb.UserId{
OpaqueId: id,
Idp: idp,
// the remote user is a federated account for the local reva
Type: userpb.UserType_USER_TYPE_FEDERATED,
}, nil
}
log.Info().Msg("Share created.")
func getIDAndMeshProvider(user string) (string, string, error) {
// the user is in the form of dimitri@apiwise.nl
split := strings.Split(user, "@")
if len(split) < 2 {
return "", "", errors.New("not in the form <id>@<provider>")
}
return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil
}
func getCreateShareRequest(r *http.Request) (*createShareRequest, error) {
var req createShareRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
return nil, errors.New("body request not recognised")
}
// validate the request
if err := validate.Struct(req); err != nil {
return nil, err
}
return &req, nil
}
func getResourceTypeFromOCMRequest(t string) providerpb.ResourceType {
switch t {
case "file":
return providerpb.ResourceType_RESOURCE_TYPE_FILE
case "folder":
return providerpb.ResourceType_RESOURCE_TYPE_CONTAINER
default:
return providerpb.ResourceType_RESOURCE_TYPE_INVALID
}
}
func getOCMShareType(t string) ocm.ShareType {
if t == "user" {
return ocm.ShareType_SHARE_TYPE_USER
}
return ocm.ShareType_SHARE_TYPE_GROUP
}
func getProtocols(p Protocols) []*ocm.Protocol {
prot := make([]*ocm.Protocol, 0, len(p))
for _, data := range p {
prot = append(prot, data.ToOCMProtocol())
}
return prot
}
@@ -16,6 +16,7 @@ type Config struct {
// /users/<first char of username>/<username>/docs
WebdavNamespace string `mapstructure:"webdav_namespace"`
SharesNamespace string `mapstructure:"shares_namespace"`
OCMNamespace string `mapstructure:"ocm_namespace"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
@@ -22,6 +22,7 @@ import (
"context"
"net/http"
"path"
"path/filepath"
"strings"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -56,6 +57,7 @@ type DavHandler struct {
PublicFolderHandler *WebDavHandler
PublicFileHandler *PublicFileHandler
SharesHandler *WebDavHandler
OCMSharesHandler *WebDavHandler
}
func (h *DavHandler) init(c *config.Config) error {
@@ -95,6 +97,11 @@ func (h *DavHandler) init(c *config.Config) error {
return err
}
h.OCMSharesHandler = new(WebDavHandler)
if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil {
return err
}
return nil
}
@@ -170,6 +177,60 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.MetaHandler.Handler(s).ServeHTTP(w, r)
case "ocm":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
c, err := s.gatewaySelector.Next()
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
// OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the
// shared token.
var token string
username, _, ok := r.BasicAuth()
if ok {
// OCM 1.0
token = username
r.URL.Path = filepath.Join("/", token, r.URL.Path)
ctx = context.WithValue(ctx, net.CtxOCM10, true)
} else {
token, _ = router.ShiftPath(r.URL.Path)
ctx = context.WithValue(ctx, net.CtxOCM10, false)
}
authRes, err := handleOCMAuth(ctx, c, token)
switch {
case err != nil:
log.Error().Err(err).Msg("error during ocm authentication")
w.WriteHeader(http.StatusInternalServerError)
return
case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
log.Debug().Str("token", token).Msg("permission denied")
fallthrough
case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
log.Debug().Str("token", token).Msg("unauthorized")
w.WriteHeader(http.StatusUnauthorized)
return
case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
log.Debug().Str("token", token).Msg("not found")
w.WriteHeader(http.StatusNotFound)
return
case authRes.Status.Code != rpc.Code_CODE_OK:
log.Error().Str("token", token).Interface("status", authRes.Status).Msg("grpc auth request failed")
w.WriteHeader(http.StatusInternalServerError)
return
}
ctx = ctxpkg.ContextSetToken(ctx, authRes.Token)
ctx = ctxpkg.ContextSetUser(ctx, authRes.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token)
log.Debug().Str("token", token).Interface("user", authRes.User).Msg("OCM user authenticated")
r = r.WithContext(ctx)
h.OCMSharesHandler.Handler(s).ServeHTTP(w, r)
case "trash-bin":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
@@ -315,3 +376,10 @@ func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1
return c.Authenticate(ctx, &authenticateRequest)
}
func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token string) (*gatewayv1beta1.AuthenticateResponse, error) {
return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
Type: "ocmshares",
ClientId: token,
})
}
@@ -35,6 +35,7 @@ type ctxKey int
const (
// CtxKeyBaseURI is the key of the base URI context field
CtxKeyBaseURI ctxKey = iota
CtxOCM10
// NsDav is the Dav ns
NsDav = "DAV:"
@@ -157,7 +157,7 @@ func (s *svc) Close() error {
}
func (s *svc) Unprotected() []string {
return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/"}
return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"}
}
func (s *svc) Handler() http.Handler {
@@ -47,6 +47,9 @@ type Config struct {
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
SkipUpdatingExistingSharesMountpoints bool `mapstructure:"skip_updating_existing_shares_mountpoint"`
EnableDenials bool `mapstructure:"enable_denials"`
OCMMountPoint string `mapstructure:"ocm_mount_point"`
ListOCMShares bool `mapstructure:"list_ocm_shares"`
Notifications map[string]interface{} `mapstructure:"notifications"`
}
// Init sets sane defaults
@@ -26,6 +26,8 @@ import (
"path"
"time"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/mime"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/user"
@@ -33,6 +35,7 @@ import (
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"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
publicsharemgr "github.com/cs3org/reva/v2/pkg/publicshare/manager/registry"
@@ -303,6 +306,82 @@ func PublicShare2ShareData(share *link.PublicShare, r *http.Request, publicURL s
return sd
}
func formatRemoteUser(u *userpb.UserId) string {
return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp)
}
func webdavInfo(protocols []*ocm.Protocol) (*ocm.WebDAVProtocol, bool) {
for _, p := range protocols {
if opt, ok := p.Term.(*ocm.Protocol_WebdavOptions); ok {
return opt.WebdavOptions, true
}
}
return nil, false
}
// ReceivedOCMShare2ShareData converts a cs3 ocm received share into a share data model.
func ReceivedOCMShare2ShareData(share *ocm.ReceivedShare, path string) (*ShareData, error) {
webdav, ok := webdavInfo(share.Protocols)
if !ok {
return nil, errtypes.InternalError("webdav endpoint not in share")
}
s := &ShareData{
ID: share.Id.OpaqueId,
UIDOwner: formatRemoteUser(share.Creator),
UIDFileOwner: formatRemoteUser(share.Owner),
ShareWith: share.Grantee.GetUserId().OpaqueId,
Permissions: RoleFromResourcePermissions(webdav.GetPermissions().GetPermissions(), false).OCSPermissions(),
ShareType: ShareTypeFederatedCloudShare,
Path: path,
FileTarget: path,
MimeType: mime.Detect(share.ResourceType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, share.Name),
ItemType: ResourceType(share.ResourceType).String(),
ItemSource: path,
STime: share.Ctime.Seconds,
Name: share.Name,
}
if share.Expiration != nil {
s.Expiration = timestampToExpiration(share.Expiration)
}
return s, nil
}
func webdavAMInfo(methods []*ocm.AccessMethod) (*ocm.WebDAVAccessMethod, bool) {
for _, a := range methods {
if opt, ok := a.Term.(*ocm.AccessMethod_WebdavOptions); ok {
return opt.WebdavOptions, true
}
}
return nil, false
}
// OCMShare2ShareData converts a cs3 ocm share into a share data model.
func OCMShare2ShareData(share *ocm.Share) (*ShareData, error) {
webdav, ok := webdavAMInfo(share.AccessMethods)
if !ok {
return nil, errtypes.InternalError("webdav endpoint not in share")
}
s := &ShareData{
ID: share.Id.OpaqueId,
UIDOwner: share.Creator.OpaqueId,
UIDFileOwner: share.Owner.OpaqueId,
ShareWith: formatRemoteUser(share.Grantee.GetUserId()),
Permissions: RoleFromResourcePermissions(webdav.GetPermissions(), false).OCSPermissions(),
ShareType: ShareTypeFederatedCloudShare,
STime: share.Ctime.Seconds,
Name: share.Name,
}
if share.Expiration != nil {
s.Expiration = timestampToExpiration(share.Expiration)
}
return s, nil
}
// LocalUserIDToString transforms a cs3api user id into an ocs data model without domain name
// TODO ocs uses user names ... so an additional lookup is needed. see mapUserIds()
func LocalUserIDToString(userID *userpb.UserId) string {
@@ -45,6 +45,8 @@ const (
RoleSpaceEditor = "spaceeditor"
// RoleFileEditor grants editor permission on a single file.
RoleFileEditor = "file-editor"
// RoleCoowner grants co-owner permissions on a resource.
RoleCoowner = "coowner"
// RoleUploader grants uploader permission to upload onto a resource.
RoleUploader = "uploader"
// RoleManager grants manager permissions on a resource. Semantically equivalent to co-owner.
@@ -286,6 +288,34 @@ func NewFileEditorRole() *Role {
}
}
// NewCoownerRole creates a coowner role.
func NewCoownerRole() *Role {
return &Role{
Name: RoleCoowner,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListGrants: true,
ListContainer: true,
ListFileVersions: true,
ListRecycle: true,
Stat: true,
InitiateFileUpload: true,
RestoreFileVersion: true,
RestoreRecycleItem: true,
CreateContainer: true,
Delete: true,
Move: true,
PurgeRecycle: true,
AddGrant: true,
UpdateGrant: true,
RemoveGrant: true,
},
ocsPermissions: PermissionAll,
}
}
// NewUploaderRole creates an uploader role
func NewUploaderRole() *Role {
return &Role{
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,24 +19,28 @@
package shares
import (
"context"
"net/http"
"strconv"
"path/filepath"
"strings"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "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/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
)
func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, resource *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
ctx := r.Context()
c, err := pool.GetGatewayServiceClient(h.gatewayAddr)
@@ -51,7 +55,7 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque
return
}
providerInfoResp, err := c.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
providerInfoResp, err := c.GetInfoByDomain(ctx, &providerpb.GetInfoByDomainRequest{
Domain: shareWithProvider,
})
if err != nil {
@@ -59,8 +63,14 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque
return
}
if providerInfoResp.Status.Code != rpc.Code_CODE_OK {
// return proper error
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error from provider info response", errors.New(providerInfoResp.Status.Message))
return
}
remoteUserRes, err := c.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_PRIMARY},
RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_FEDERATED},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err)
@@ -71,39 +81,20 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque
return
}
createShareReq := &ocm.CreateOCMShareRequest{
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
/* TODO extend the spec with role names?
"role": {
Decoder: "plain",
Value: []byte(role.Name),
},
*/
"permissions": {
Decoder: "plain",
Value: []byte(strconv.Itoa(int(role.OCSPermissions()))),
},
"name": {
Decoder: "plain",
Value: []byte(statInfo.Path),
},
},
},
ResourceId: statInfo.Id,
Grant: &ocm.ShareGrant{
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{UserId: remoteUserRes.RemoteUser.GetId()},
},
Permissions: &ocm.SharePermissions{
Permissions: role.CS3ResourcePermissions(),
createShareResponse, err := c.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{
ResourceId: resource.Id,
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: remoteUserRes.RemoteUser.Id,
},
},
RecipientMeshProvider: providerInfoResp.ProviderInfo,
}
createShareResponse, err := c.CreateOCMShare(ctx, createShareReq)
AccessMethods: []*ocm.AccessMethod{
share.NewWebDavAccessMethod(role.CS3ResourcePermissions()),
share.NewWebappAccessMethod(getViewModeFromRole(role)),
},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create ocm share request", err)
return
@@ -117,12 +108,41 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque
return
}
response.WriteOCSSuccess(w, r, "OCM Share created")
s := createShareResponse.Share
data, err := conversions.OCMShare2ShareData(s)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error converting share", err)
return
}
h.mapUserIdsFederatedShare(ctx, c, data)
info, status, err := h.getResourceInfoByID(ctx, c, s.ResourceId)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", err)
return
}
if status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", errors.New(status.Message))
return
}
h.addFileInfo(ctx, data, info)
response.WriteOCSSuccess(w, r, data)
}
// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid}
func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) {
func getViewModeFromRole(role *conversions.Role) providerv1beta1.ViewMode {
switch role.Name {
case conversions.RoleViewer:
return providerv1beta1.ViewMode_VIEW_MODE_READ_ONLY
case conversions.RoleEditor:
return providerv1beta1.ViewMode_VIEW_MODE_READ_WRITE
}
return providerv1beta1.ViewMode_VIEW_MODE_INVALID
}
// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid}.
func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) {
// TODO: Implement response with HAL schemating
ctx := r.Context()
@@ -156,28 +176,125 @@ func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) {
response.WriteOCSSuccess(w, r, share)
}
// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares
// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares.
func (h *Handler) ListFederatedShares(w http.ResponseWriter, r *http.Request) {
// TODO Implement pagination.
// TODO Implement response with HAL schemating
ctx := r.Context()
}
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
func (h *Handler) listReceivedFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, state ocm.ShareState) ([]*conversions.ShareData, error) {
listRes, err := gw.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
return nil, err
}
listOCMSharesResponse, err := gatewayClient.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{})
shares := []*conversions.ShareData{}
for _, s := range listRes.Shares {
if state != ocsStateUnknown && s.State != state {
continue
}
sd, err := conversions.ReceivedOCMShare2ShareData(s, h.ocmLocalMount(s))
if err != nil {
continue
}
h.mapUserIdsReceivedFederatedShare(ctx, gw, sd)
sd.State = mapOCMState(s.State)
shares = append(shares, sd)
}
return shares, nil
}
func (h *Handler) ocmLocalMount(share *ocm.ReceivedShare) string {
return filepath.Join("/", h.ocmMountPoint, share.Id.OpaqueId)
}
func (h *Handler) mapUserIdsReceivedFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
if sd.ShareWith != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.ShareWith, false)
sd.ShareWith = user.Username
sd.ShareWithDisplayname = user.DisplayName
}
if sd.UIDOwner != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.UIDOwner)
sd.DisplaynameOwner = user.DisplayName
}
if sd.UIDFileOwner != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.UIDFileOwner)
sd.DisplaynameFileOwner = user.DisplayName
}
}
func (h *Handler) mapUserIdsFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
if sd.ShareWith != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.ShareWith)
sd.ShareWith = user.Username
sd.ShareWithDisplayname = user.DisplayName
}
if sd.UIDOwner != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.UIDOwner, false)
sd.DisplaynameOwner = user.DisplayName
}
if sd.UIDFileOwner != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.UIDFileOwner, false)
sd.DisplaynameFileOwner = user.DisplayName
}
}
func (h *Handler) mustGetRemoteUser(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, id string) *userIdentifiers {
s := strings.SplitN(id, "@", 2)
opaqueID, idp := s[0], s[1]
userRes, err := gw.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
RemoteUserId: &userpb.UserId{
Idp: idp,
OpaqueId: opaqueID,
},
})
if err != nil {
return &userIdentifiers{}
}
if userRes.Status.Code != rpc.Code_CODE_OK {
return &userIdentifiers{}
}
user := userRes.RemoteUser
return &userIdentifiers{
DisplayName: user.DisplayName,
Username: user.Username,
Mail: user.Mail,
}
}
func (h *Handler) listOutcomingFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, filters []*ocm.ListOCMSharesRequest_Filter) ([]*conversions.ShareData, error) {
listRes, err := gw.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{
Filters: filters,
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc list ocm share request", err)
return
return nil, err
}
shares := []*conversions.ShareData{}
for _, s := range listRes.Shares {
sd, err := conversions.OCMShare2ShareData(s)
if err != nil {
continue
}
h.mapUserIdsFederatedShare(ctx, gw, sd)
info, status, err := h.getResourceInfoByID(ctx, gw, s.ResourceId)
if err != nil {
return nil, err
}
if status.Code != rpc.Code_CODE_OK {
return nil, err
}
shares := listOCMSharesResponse.GetShares()
if shares == nil {
shares = make([]*ocm.Share, 0)
h.addFileInfo(ctx, sd, info)
shares = append(shares, sd)
}
response.WriteOCSSuccess(w, r, shares)
return shares, nil
}
@@ -46,6 +46,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response"
@@ -82,6 +83,8 @@ type Handler struct {
publicURL string
sharePrefix string
homeNamespace string
ocmMountPoint string
listOCMShares bool
skipUpdatingExistingSharesMountpoints bool
additionalInfoTemplate *template.Template
userIdentifierCache *ttlcache.Cache
@@ -132,6 +135,8 @@ func (h *Handler) Init(c *config.Config) error {
h.publicURL = c.Config.Host
h.sharePrefix = c.SharePrefix
h.homeNamespace = c.HomeNamespace
h.ocmMountPoint = c.OCMMountPoint
h.listOCMShares = c.ListOCMShares
h.skipUpdatingExistingSharesMountpoints = c.SkipUpdatingExistingSharesMountpoints
h.additionalInfoTemplate, _ = template.New("additionalInfo").Parse(c.AdditionalInfoAttribute)
@@ -861,7 +866,8 @@ const (
func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) {
// which pending state to list
stateFilter := getStateFilter(r.FormValue("state"))
state := r.FormValue("state")
stateFilter := getStateFilter(state)
showHidden, _ := strconv.ParseBool(r.URL.Query().Get("show_hidden"))
@@ -1037,6 +1043,18 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) {
sublog.Debug().Msgf("share: %+v", *data)
}
if h.listOCMShares {
// include ocm shares in the response
stateFilter := getOCMStateFilter(state)
lst, err := h.listReceivedFederatedShares(ctx, client, stateFilter)
if err != nil {
sublog.Err(err).Msg("error listing received ocm shares")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing received ocm shares", err)
return
}
shares = append(shares, lst...)
}
response.WriteOCSSuccess(w, r, shares)
}
@@ -1520,6 +1538,19 @@ func mapState(state collaboration.ShareState) int {
return mapped
}
func mapOCMState(state ocmv1beta1.ShareState) int {
switch state {
case ocmv1beta1.ShareState_SHARE_STATE_PENDING:
return ocsStatePending
case ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED:
return ocsStateAccepted
case ocmv1beta1.ShareState_SHARE_STATE_REJECTED:
return ocsStateRejected
default:
return ocsStateUnknown
}
}
func getStateFilter(s string) collaboration.ShareState {
var stateFilter collaboration.ShareState
switch s {
@@ -1537,6 +1568,21 @@ func getStateFilter(s string) collaboration.ShareState {
return stateFilter
}
func getOCMStateFilter(s string) ocmv1beta1.ShareState {
switch s {
case "all":
return ocsStateUnknown // no filter
case "0": // accepted
return ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED
case "1": // pending
return ocmv1beta1.ShareState_SHARE_STATE_PENDING
case "2": // rejected
return ocmv1beta1.ShareState_SHARE_STATE_REJECTED
default:
return ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED
}
}
func (h *Handler) getPoolClient() (gateway.GatewayAPIClient, error) {
return pool.GetGatewayServiceClient(h.gatewayAddr)
}
@@ -25,6 +25,7 @@ import (
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
@@ -252,7 +253,46 @@ func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filte
log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped")
ocsDataPayload = append(ocsDataPayload, data)
}
if h.listOCMShares {
// include the ocm shares
ocmShares, err := h.listOutcomingFederatedShares(ctx, client, convertToOCMFilters(filters))
if err != nil {
return nil, nil, err
}
ocsDataPayload = append(ocsDataPayload, ocmShares...)
}
}
return ocsDataPayload, nil, nil
}
func convertToOCMFilters(filters []*collaboration.Filter) []*ocmpb.ListOCMSharesRequest_Filter {
ocmfilters := []*ocmpb.ListOCMSharesRequest_Filter{}
for _, f := range filters {
switch v := f.Term.(type) {
case *collaboration.Filter_ResourceId:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &ocmpb.ListOCMSharesRequest_Filter_ResourceId{
ResourceId: v.ResourceId,
},
})
case *collaboration.Filter_Creator:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_CREATOR,
Term: &ocmpb.ListOCMSharesRequest_Filter_Creator{
Creator: v.Creator,
},
})
case *collaboration.Filter_Owner:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_OWNER,
Term: &ocmpb.ListOCMSharesRequest_Filter_Owner{
Owner: v.Owner,
},
})
}
}
return ocmfilters
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmd
package reqres
import (
"encoding/json"
@@ -25,10 +25,10 @@ import (
"github.com/cs3org/reva/v2/pkg/appctx"
)
// APIErrorCode stores the type of error encountered
// APIErrorCode stores the type of error encountered.
type APIErrorCode string
// The various types of errors that can be expected to occur
// The various types of errors that can be expected to occur.
const (
APIErrorNotFound APIErrorCode = "RESOURCE_NOT_FOUND"
APIErrorUnauthenticated APIErrorCode = "UNAUTHENTICATED"
@@ -36,10 +36,11 @@ const (
APIErrorUnimplemented APIErrorCode = "FUNCTION_NOT_IMPLEMENTED"
APIErrorInvalidParameter APIErrorCode = "INVALID_PARAMETER"
APIErrorProviderError APIErrorCode = "PROVIDER_ERROR"
APIErrorAlreadyExist APIErrorCode = "ALREADY_EXIST"
APIErrorServerError APIErrorCode = "SERVER_ERROR"
)
// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes
// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes.
var APIErrorCodeMapping = map[APIErrorCode]int{
APIErrorNotFound: http.StatusNotFound,
APIErrorUnauthenticated: http.StatusUnauthorized,
@@ -47,16 +48,17 @@ var APIErrorCodeMapping = map[APIErrorCode]int{
APIErrorUnimplemented: http.StatusNotImplemented,
APIErrorInvalidParameter: http.StatusBadRequest,
APIErrorProviderError: http.StatusBadGateway,
APIErrorAlreadyExist: http.StatusConflict,
APIErrorServerError: http.StatusInternalServerError,
}
// APIError encompasses the error type and message
// APIError encompasses the error type and message.
type APIError struct {
Code APIErrorCode `json:"code"`
Message string `json:"message"`
}
// WriteError handles writing error responses
// WriteError handles writing error responses.
func WriteError(w http.ResponseWriter, r *http.Request, code APIErrorCode, message string, e error) {
if e != nil {
appctx.GetLogger(r.Context()).Error().Err(e).Msg(message)
@@ -0,0 +1,139 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
)
type appsHandler struct {
gatewayClient gateway.GatewayAPIClient
ocmMountPoint string
}
func (h *appsHandler) init(c *config) error {
var err error
h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return err
}
h.ocmMountPoint = c.OCMMountPoint
return nil
}
func (h *appsHandler) shareInfo(p string) (*ocmpb.ShareId, string) {
p = strings.TrimPrefix(p, h.ocmMountPoint)
shareID, rel := router.ShiftPath(p)
if len(rel) > 0 {
rel = rel[1:]
}
return &ocmpb.ShareId{OpaqueId: shareID}, rel
}
func (h *appsHandler) OpenInApp(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := r.ParseForm(); err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "parameters could not be parsed", nil)
return
}
path := r.Form.Get("file")
if path == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing file", nil)
return
}
shareID, rel := h.shareInfo(path)
template, err := h.webappTemplate(ctx, shareID)
if err != nil {
var e errtypes.NotFound
if errors.As(err, &e) {
reqres.WriteError(w, r, reqres.APIErrorNotFound, e.Error(), nil)
}
reqres.WriteError(w, r, reqres.APIErrorServerError, err.Error(), err)
return
}
url := resolveTemplate(template, rel)
if err := json.NewEncoder(w).Encode(map[string]any{
"app_url": url,
}); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (string, error) {
res, err := h.gatewayClient.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: id,
},
},
})
if err != nil {
return "", err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
return "", errtypes.NotFound(res.Status.Message)
}
return "", errtypes.InternalError(res.Status.Message)
}
webapp, ok := getWebappProtocol(res.Share.Protocols)
if !ok {
return "", errtypes.BadRequest("share does not contain webapp protocol")
}
return webapp.UriTemplate, nil
}
func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) {
for _, p := range protocols {
if t, ok := p.Term.(*ocmpb.Protocol_WebappOptions); ok {
return t.WebappOptions, true
}
}
return nil, false
}
func resolveTemplate(template string, rel string) string {
// the template is of type "https://open-cloud-mesh.org/s/share-hash/{relative-path-to-shared-resource}"
return strings.Replace(template, "{relative-path-to-shared-resource}", rel, 1)
}
@@ -0,0 +1,119 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"bytes"
"html/template"
"io"
"os"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
type emailParams struct {
User *userpb.User
Token string
MeshDirectoryURL string
InviteLink string
}
const defaultSubject = `ScienceMesh: {{.User.DisplayName}} wants to collaborate with you`
const defaultBody = `Hi
{{.User.DisplayName}} ({{.User.Mail}}) wants to start sharing OCM resources with you.
To accept the invite, please visit the following URL:
{{.InviteLink}}
Alternatively, you can visit your mesh provider and use the following details:
Token: {{.Token}}
ProviderDomain: {{.User.Id.Idp}}
Best,
The ScienceMesh team`
func (h *tokenHandler) sendEmail(recipient string, obj *emailParams) error {
subj, err := h.generateEmailSubject(obj)
if err != nil {
return err
}
body, err := h.generateEmailBody(obj)
if err != nil {
return err
}
return h.smtpCredentials.SendMail(recipient, subj, body)
}
func (h *tokenHandler) generateEmailSubject(obj *emailParams) (string, error) {
var buf bytes.Buffer
err := h.tplSubj.Execute(&buf, obj)
return buf.String(), err
}
func (h *tokenHandler) generateEmailBody(obj *emailParams) (string, error) {
var buf bytes.Buffer
err := h.tplBody.Execute(&buf, obj)
return buf.String(), err
}
func (h *tokenHandler) initBodyTemplate(bodyTemplPath string) error {
var body string
if bodyTemplPath == "" {
body = defaultBody
} else {
f, err := os.Open(bodyTemplPath)
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return err
}
body = string(data)
}
tpl, err := template.New("tpl_body").Parse(body)
if err != nil {
return err
}
h.tplBody = tpl
return nil
}
func (h *tokenHandler) initSubjectTemplate(subjTempl string) error {
var subj string
if subjTempl == "" {
subj = defaultSubject
} else {
subj = subjTempl
}
tpl, err := template.New("tpl_subj").Parse(subj)
if err != nil {
return err
}
h.tplSubj = tpl
return nil
}
@@ -0,0 +1,87 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"encoding/json"
"errors"
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
)
type providersHandler struct {
gatewayClient gateway.GatewayAPIClient
}
func (h *providersHandler) init(c *config) error {
var err error
h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return err
}
return nil
}
type provider struct {
FullName string `json:"full_name"`
Domain string `json:"domain"`
}
// ListProviders lists all the providers filtering by the `search` query parameter.
func (h *providersHandler) ListProviders(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
term := strings.ToLower(r.URL.Query().Get("search"))
listRes, err := h.gatewayClient.ListAllProviders(ctx, &providerpb.ListAllProvidersRequest{})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing all providers", err)
return
}
if listRes.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorServerError, listRes.Status.Message, errors.New(listRes.Status.Message))
return
}
filtered := []*provider{}
for _, p := range listRes.Providers {
if strings.Contains(strings.ToLower(p.FullName), term) ||
strings.Contains(strings.ToLower(p.Domain), term) {
filtered = append(filtered, &provider{
FullName: p.FullName,
Domain: p.Domain,
})
}
}
if err := json.NewEncoder(w).Encode(filtered); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response in json", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
@@ -0,0 +1,136 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"net/http"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rhttp/global"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/smtpclient"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
)
func init() {
global.Register("sciencemesh", New)
}
// New returns a new sciencemesh service.
func New(m map[string]interface{}, _ *zerolog.Logger) (global.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
r := chi.NewRouter()
s := &svc{
conf: &c,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
return s, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
type config struct {
Prefix string `mapstructure:"prefix"`
SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials" validate:"required"`
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
MeshDirectoryURL string `mapstructure:"mesh_directory_url" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required"`
SubjectTemplate string `mapstructure:"subject_template"`
BodyTemplatePath string `mapstructure:"body_template_path"`
OCMMountPoint string `mapstructure:"ocm_mount_point"`
}
func (c *config) ApplyDefaults() {
if c.Prefix == "" {
c.Prefix = "sciencemesh"
}
if c.OCMMountPoint == "" {
c.OCMMountPoint = "/ocm"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
type svc struct {
conf *config
router chi.Router
}
func (s *svc) routerInit() error {
tokenHandler := new(tokenHandler)
if err := tokenHandler.init(s.conf); err != nil {
return err
}
providersHandler := new(providersHandler)
if err := providersHandler.init(s.conf); err != nil {
return err
}
sharesHandler := new(sharesHandler)
if err := sharesHandler.init(s.conf); err != nil {
return err
}
appsHandler := new(appsHandler)
if err := appsHandler.init(s.conf); err != nil {
return err
}
s.router.Get("/generate-invite", tokenHandler.Generate)
s.router.Get("/list-invite", tokenHandler.ListInvite)
s.router.Post("/accept-invite", tokenHandler.AcceptInvite)
s.router.Get("/find-accepted-users", tokenHandler.FindAccepted)
s.router.Delete("/delete-accepted-user", tokenHandler.DeleteAccepted)
s.router.Get("/list-providers", providersHandler.ListProviders)
s.router.Post("/create-share", sharesHandler.CreateShare)
s.router.Post("/open-in-app", appsHandler.OpenInApp)
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return nil
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Str("path", r.URL.Path).Msg("sciencemesh routing")
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
r.URL.RawPath = ""
s.router.ServeHTTP(w, r)
})
}
@@ -0,0 +1,179 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"encoding/json"
"errors"
"mime"
"net/http"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/go-playground/validator/v10"
)
var validate = validator.New()
type sharesHandler struct {
gatewayClient gateway.GatewayAPIClient
}
func (h *sharesHandler) init(c *config) error {
var err error
h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc)
return err
}
type createShareRequest struct {
SourcePath string `json:"sourcePath" validate:"required"`
TargetPath string `json:"targetPath" validate:"required"`
Type string `json:"type"`
Role string `json:"role" validate:"oneof=viewer editor"`
RecipientUsername string `json:"recipientUsername" validate:"required"`
RecipientHost string `json:"recipientHost" validate:"required"`
}
// CreateShare creates an OCM share.
func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
req, err := getCreateShareRequest(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "invalid parameters", err)
return
}
ctx := r.Context()
statRes, err := h.gatewayClient.Stat(ctx, &providerpb.StatRequest{
Ref: &providerpb.Reference{
Path: req.SourcePath,
},
})
switch {
case err != nil:
reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error", err)
return
case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
reqres.WriteError(w, r, reqres.APIErrorNotFound, statRes.Status.Message, nil)
return
case statRes.Status.Code != rpc.Code_CODE_OK:
reqres.WriteError(w, r, reqres.APIErrorServerError, statRes.Status.Message, errors.New(statRes.Status.Message))
return
}
recipientProviderInfo, err := h.gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: req.RecipientHost,
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err)
return
}
if recipientProviderInfo.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorNotFound, recipientProviderInfo.Status.Message, errors.New(recipientProviderInfo.Status.Message))
return
}
perm, viewMode := getPermissionsByRole(req.Role)
log.Debug().Msg("calling gatewayClient.CreateOCMShare from sciencemesh/share.go")
shareRes, err := h.gatewayClient.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{
ResourceId: statRes.Info.Id,
Grantee: &providerpb.Grantee{
Type: providerpb.GranteeType_GRANTEE_TYPE_USER,
Id: &providerpb.Grantee_UserId{
UserId: &userpb.UserId{
Idp: req.RecipientHost,
OpaqueId: req.RecipientUsername,
},
},
},
RecipientMeshProvider: recipientProviderInfo.ProviderInfo,
AccessMethods: []*ocm.AccessMethod{
share.NewWebDavAccessMethod(perm),
share.NewWebappAccessMethod(viewMode),
},
})
log.Debug().Msg("called gatewayClient.CreateOCMShare from sciencemesh/share.go")
switch {
case err != nil:
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc CreateOCMShare", err)
return
case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
reqres.WriteError(w, r, reqres.APIErrorNotFound, shareRes.Status.Message, nil)
return
case shareRes.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, shareRes.Status.Message, nil)
return
case shareRes.Status.Code != rpc.Code_CODE_OK:
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, shareRes.Status.Message, errors.New(shareRes.Status.Message))
return
}
if err := json.NewEncoder(w).Encode(shareRes); err != nil {
log.Error().Err(err).Msg("error encoding response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func getPermissionsByRole(role string) (*providerpb.ResourcePermissions, appprovider.ViewMode) {
switch role {
case "viewer":
return conversions.NewViewerRole(false).CS3ResourcePermissions(), appprovider.ViewMode_VIEW_MODE_READ_ONLY
case "editor":
return conversions.NewEditorRole(false).CS3ResourcePermissions(), appprovider.ViewMode_VIEW_MODE_READ_WRITE
}
return nil, 0
}
func getCreateShareRequest(r *http.Request) (*createShareRequest, error) {
var req createShareRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
return nil, errors.New("body request not recognised")
}
// set defaults
if req.Type == "" {
req.Type = "viewer"
}
// validate the request
if err := validate.Struct(req); err != nil {
return nil, err
}
return &req, nil
}
@@ -0,0 +1,324 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sciencemesh
import (
"encoding/json"
"errors"
"html/template"
"mime"
"net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/smtpclient"
"github.com/cs3org/reva/v2/pkg/utils/list"
)
type tokenHandler struct {
gatewayClient gateway.GatewayAPIClient
smtpCredentials *smtpclient.SMTPCredentials
meshDirectoryURL string
providerDomain string
tplSubj *template.Template
tplBody *template.Template
}
func (h *tokenHandler) init(c *config) error {
var err error
h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return err
}
if c.SMTPCredentials != nil {
h.smtpCredentials = smtpclient.NewSMTPCredentials(c.SMTPCredentials)
}
h.meshDirectoryURL = c.MeshDirectoryURL
h.providerDomain = c.ProviderDomain
if err := h.initSubjectTemplate(c.SubjectTemplate); err != nil {
return err
}
if err := h.initBodyTemplate(c.BodyTemplatePath); err != nil {
return err
}
return nil
}
type token struct {
Token string `json:"token"`
Description string `json:"description,omitempty"`
Expiration uint64 `json:"expiration,omitempty"`
InviteLink string `json:"invite_link"`
}
// Generate generates an invitation token and if a recipient is specified,
// will send an email containing the link the user will use to accept the
// invitation.
func (h *tokenHandler) Generate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
query := r.URL.Query()
token, err := h.gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{
Description: query.Get("description"),
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error generating token", err)
return
}
user := ctxpkg.ContextMustGetUser(ctx)
recipient := query.Get("recipient")
if recipient != "" && h.smtpCredentials != nil {
templObj := &emailParams{
User: user,
Token: token.InviteToken.Token,
MeshDirectoryURL: h.meshDirectoryURL,
}
if err := h.sendEmail(recipient, templObj); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending token by mail", err)
return
}
}
tknRes := h.prepareGenerateTokenResponse(token.InviteToken)
if err := json.NewEncoder(w).Encode(tknRes); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (h *tokenHandler) prepareGenerateTokenResponse(tkn *invitepb.InviteToken) *token {
res := &token{
Token: tkn.Token,
Description: tkn.Description,
InviteLink: h.meshDirectoryURL + "?token=" + tkn.Token + "&providerDomain=" + h.providerDomain,
}
if tkn.Expiration != nil {
res.Expiration = tkn.Expiration.Seconds
}
return res
}
type acceptInviteRequest struct {
Token string `json:"token"`
ProviderDomain string `json:"providerDomain"`
}
// AcceptInvite accepts an invitation from the user in the remote provider.
func (h *tokenHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
req, err := getAcceptInviteRequest(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
return
}
if req.Token == "" || req.ProviderDomain == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token and providerDomain must not be null", nil)
return
}
providerInfo, err := h.gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: req.ProviderDomain,
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err)
return
}
if providerInfo.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorServerError, "grpc forward invite request failed", errors.New(providerInfo.Status.Message))
return
}
forwardInviteReq := &invitepb.ForwardInviteRequest{
InviteToken: &invitepb.InviteToken{
Token: req.Token,
},
OriginSystemProvider: providerInfo.ProviderInfo,
}
forwardInviteResponse, err := h.gatewayClient.ForwardInvite(ctx, forwardInviteReq)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc forward invite request", err)
return
}
if forwardInviteResponse.Status.Code != rpc.Code_CODE_OK {
switch forwardInviteResponse.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil)
return
case rpc.Code_CODE_INVALID_ARGUMENT:
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil)
return
case rpc.Code_CODE_ALREADY_EXISTS:
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil)
return
case rpc.Code_CODE_PERMISSION_DENIED:
reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "remove service not trusted", nil)
return
default:
reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+forwardInviteResponse.Status.Message, errors.New(forwardInviteResponse.Status.Message))
return
}
}
w.WriteHeader(http.StatusOK)
log.Info().Str("token", req.Token).Str("provider", req.ProviderDomain).Msgf("invite forwarded")
}
func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) {
var req acceptInviteRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
req.Token, req.ProviderDomain = r.FormValue("token"), r.FormValue("providerDomain")
}
return &req, nil
}
type remoteUser struct {
DisplayName string `json:"display_name"`
Idp string `json:"idp"`
UserID string `json:"user_id"`
Mail string `json:"mail"`
}
// FindAccepted returns the list of all the users that accepted the invitation
// to the authenticated user.
func (h *tokenHandler) FindAccepted(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
res, err := h.gatewayClient.FindAcceptedUsers(ctx, &invitepb.FindAcceptedUsersRequest{})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc find accepted users request", err)
return
}
users := list.Map(res.AcceptedUsers, func(u *userpb.User) *remoteUser {
return &remoteUser{
DisplayName: u.DisplayName,
Idp: u.Id.Idp,
UserID: u.Id.OpaqueId,
Mail: u.Mail,
}
})
if err := json.NewEncoder(w).Encode(users); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
// DeleteAccepted deletes the given user from the list of the accepted users.
func (h *tokenHandler) DeleteAccepted(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := getDeleteAcceptedRequest(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
return
}
res, err := h.gatewayClient.DeleteAcceptedUser(ctx, &invitepb.DeleteAcceptedUserRequest{
RemoteUserId: &userpb.UserId{
Idp: req.Idp,
OpaqueId: req.UserID,
Type: userpb.UserType_USER_TYPE_FEDERATED,
},
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorServerError, "grpc forward invite request failed", errors.New(res.Status.Message))
return
}
w.WriteHeader(http.StatusOK)
}
type deleteAcceptedRequest struct {
Idp string `json:"idp"`
UserID string `json:"user_id"`
}
func getDeleteAcceptedRequest(r *http.Request) (*deleteAcceptedRequest, error) {
var req deleteAcceptedRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
req.Idp, req.UserID = r.FormValue("idp"), r.FormValue("user_id")
}
return &req, nil
}
func (h *tokenHandler) ListInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
res, err := h.gatewayClient.ListInviteTokens(ctx, &invitepb.ListInviteTokensRequest{})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing tokens", err)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorServerError, res.Status.Message, errors.New(res.Status.Message))
return
}
tokens := make([]*token, 0, len(res.InviteTokens))
for _, tkn := range res.InviteTokens {
tokens = append(tokens, h.prepareGenerateTokenResponse(tkn))
}
if err := json.NewEncoder(w).Encode(tokens); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
+1 -1
View File
@@ -40,6 +40,6 @@ type Registry interface {
// Provider is the interface that application providers implement
// for interacting with external apps that serve the requested resource.
type Provider interface {
GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error)
GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error)
GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error)
}
+1 -1
View File
@@ -39,7 +39,7 @@ type demoProvider struct {
iframeUIProvider string
}
func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
url := fmt.Sprintf("<iframe src=%s/open/%s?view-mode=%s&access-token=%s />", p.iframeUIProvider, storagespace.FormatResourceID(*resource.Id), viewMode.String(), token)
return &appprovider.OpenInAppURL{
AppUrl: url,
+1 -1
View File
@@ -122,7 +122,7 @@ func New(m map[string]interface{}) (app.Provider, error) {
}, nil
}
func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
log := appctx.GetLogger(ctx)
ext := path.Ext(resource.Path)
+1
View File
@@ -27,6 +27,7 @@ import (
_ "github.com/cs3org/reva/v2/pkg/auth/manager/ldap"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/machine"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/nextcloud"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/ocmshares"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/oidc"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/owncloudsql"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/publicshares"
@@ -0,0 +1,187 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmshares
import (
"context"
provider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocminvite "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/auth"
"github.com/cs3org/reva/v2/pkg/auth/manager/registry"
"github.com/cs3org/reva/v2/pkg/auth/scope"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
)
func init() {
registry.Register("ocmshares", New)
}
type manager struct {
c *config
gw gateway.GatewayAPIClient
}
type config struct {
GatewayAddr string `mapstructure:"gatewaysvc"`
}
func (c *config) ApplyDefaults() {
c.GatewayAddr = sharedconf.GetGatewaySVC(c.GatewayAddr)
}
// New creates a new ocmshares authentication manager.
func New(m map[string]interface{}) (auth.Manager, error) {
var mgr manager
if err := mgr.Configure(m); err != nil {
return nil, err
}
gw, err := pool.GetGatewayServiceClient(mgr.c.GatewayAddr)
if err != nil {
return nil, err
}
mgr.gw = gw
return &mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
var c config
if err := cfg.Decode(ml, &c); err != nil {
return errors.Wrap(err, "ocmshares: error decoding config")
}
m.c = &c
return nil
}
func (m *manager) Authenticate(ctx context.Context, token, _ string) (*userpb.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx).With().Str("token", token).Logger()
shareRes, err := m.gw.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{
Token: token,
})
switch {
case err != nil:
log.Error().Err(err).Msg("error getting ocm share by token")
return nil, nil, err
case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
log.Debug().Msg("ocm share not found")
return nil, nil, errtypes.NotFound(shareRes.Status.Message)
case shareRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
log.Debug().Msg("permission denied")
return nil, nil, errtypes.InvalidCredentials(shareRes.Status.Message)
case shareRes.Status.Code != rpc.Code_CODE_OK:
log.Error().Interface("status", shareRes.Status).Msg("got unexpected error in the grpc call to GetOCMShare")
return nil, nil, errtypes.InternalError(shareRes.Status.Message)
}
// the user authenticated using the ocmshares authentication method
// is the recipient of the share
u := shareRes.Share.Grantee.GetUserId()
d, err := utils.MarshalProtoV1ToJSON(shareRes.GetShare().Creator)
if err != nil {
return nil, nil, err
}
o := &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"user-filter": {
Decoder: "json",
Value: d,
},
},
}
userRes, err := m.gw.GetAcceptedUser(ctx, &ocminvite.GetAcceptedUserRequest{
RemoteUserId: u,
Opaque: o,
})
switch {
case err != nil:
return nil, nil, err
case userRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(shareRes.Status.Message)
case userRes.Status.Code != rpc.Code_CODE_OK:
return nil, nil, errtypes.InternalError(userRes.Status.Message)
}
role, roleStr := getRole(shareRes.Share)
scope, err := scope.AddOCMShareScope(shareRes.Share, role, nil)
if err != nil {
return nil, nil, err
}
user := userRes.RemoteUser
user.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"ocm-share-role": {
Decoder: "plain",
Value: []byte(roleStr),
},
},
}
return user, scope, nil
}
func getRole(s *ocm.Share) (authpb.Role, string) {
// TODO: consider to somehow merge the permissions from all the access methods?
// it's not clear infact which should be the role when webdav is editor role while
// webapp is only view mode for example
// this implementation considers only the simple case in which when a client creates
// a share with multiple access methods, the permissions are matching in all of them.
for _, m := range s.AccessMethods {
switch v := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
p := v.WebdavOptions.Permissions
if p.InitiateFileUpload {
return authpb.Role_ROLE_EDITOR, "editor"
}
if p.InitiateFileDownload {
return authpb.Role_ROLE_VIEWER, "viewer"
}
case *ocm.AccessMethod_WebappOptions:
viewMode := v.WebappOptions.ViewMode
if viewMode == provider.ViewMode_VIEW_MODE_VIEW_ONLY ||
viewMode == provider.ViewMode_VIEW_MODE_READ_ONLY ||
viewMode == provider.ViewMode_VIEW_MODE_PREVIEW {
return authpb.Role_ROLE_VIEWER, "viewer"
}
if viewMode == provider.ViewMode_VIEW_MODE_READ_WRITE {
return authpb.Role_ROLE_EDITOR, "editor"
}
}
}
return authpb.Role_ROLE_INVALID, "invalid"
}
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package scope
import (
"context"
"path/filepath"
"strings"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
// FIXME: the namespace here is hardcoded
// find a way to pass it from the config.
const ocmNamespace = "/ocm"
func ocmShareScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) {
var share ocmv1beta1.Share
if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
return false, err
}
switch v := resource.(type) {
// viewer role
case *registry.ListStorageProvidersRequest:
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.StorageId = string(e.Value)
}
if e, ok := v.Opaque.Map["space_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.SpaceId = string(e.Value)
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return checkStorageRefForOCMShare(&share, ref, ocmNamespace), nil
case *registry.GetStorageProvidersRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.StatRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.ListContainerRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.InitiateFileDownloadRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *appprovider.OpenInAppRequest:
return checkStorageRefForOCMShare(&share, &provider.Reference{ResourceId: v.ResourceInfo.Id}, ocmNamespace), nil
case *gateway.OpenInAppRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.GetLockRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
// editor role
case *provider.CreateContainerRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.TouchFileRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.DeleteRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.MoveRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetSource(), ocmNamespace) && checkStorageRefForOCMShare(&share, v.GetDestination(), ocmNamespace), nil
case *provider.InitiateFileUploadRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.SetArbitraryMetadataRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.UnsetArbitraryMetadataRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.SetLockRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.RefreshLockRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.UnlockRequest:
return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
// App provider requests
case *appregistry.GetDefaultAppProviderForMimeTypeRequest:
return true, nil
case *appregistry.GetAppProvidersRequest:
return true, nil
case *userv1beta1.GetUserByClaimRequest:
return true, nil
case *userv1beta1.GetUserRequest:
return true, nil
case *provider.ListStorageSpacesRequest:
return true, nil
case *ocmv1beta1.GetOCMShareRequest:
return checkOCMShareRef(&share, v.GetRef()), nil
case *ocmv1beta1.GetOCMShareByTokenRequest:
return share.Token == v.GetToken(), nil
case string:
return checkResourcePath(v), nil
}
return false, nil
}
func checkStorageRefForOCMShare(s *ocmv1beta1.Share, r *provider.Reference, ns string) bool {
if r.ResourceId != nil {
return utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) || strings.HasPrefix(r.ResourceId.OpaqueId, s.Token)
}
// FIXME: the paths here are hardcoded
if strings.HasPrefix(r.GetPath(), "/public/"+s.Token) {
return true
}
return strings.HasPrefix(r.GetPath(), filepath.Join(ns, s.Token))
}
func checkOCMShareRef(s *ocmv1beta1.Share, ref *ocmv1beta1.ShareReference) bool {
return ref.GetToken() == s.Token
}
// AddOCMShareScope adds the scope to allow access to an OCM share and the share resource.
func AddOCMShareScope(share *ocmv1beta1.Share, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
// Create a new "scope share" to only expose the required fields `ResourceId` and `Token` to the scope.
scopeShare := ocmv1beta1.Share{ResourceId: share.ResourceId, Token: share.Token}
val, err := utils.MarshalProtoV1ToJSON(&scopeShare)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["ocmshare:"+share.Id.OpaqueId] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
}
return scopes, nil
}
// GetOCMSharesFromScopes returns all OCM shares in the given scope.
func GetOCMSharesFromScopes(scopes map[string]*authpb.Scope) ([]*ocmv1beta1.Share, error) {
var shares []*ocmv1beta1.Share
for k, s := range scopes {
if strings.HasPrefix(k, "ocmshare:") {
res := s.Resource
if res.Decoder != "json" {
return nil, errtypes.InternalError("resource should be json encoded")
}
var share ocmv1beta1.Share
err := utils.UnmarshalJSONToProtoV1(res.Value, &share)
if err != nil {
return nil, err
}
shares = append(shares, &share)
}
}
return shares, nil
}
+1
View File
@@ -37,6 +37,7 @@ var supportedScopes = map[string]Verifier{
"share": shareScope,
"receivedshare": receivedShareScope,
"lightweight": lightweightAccountScope,
"ocmshare": ocmShareScope,
}
// VerifyScope is the function to be called when dismantling tokens to check if
+32 -8
View File
@@ -31,6 +31,7 @@ import (
"golang.org/x/crypto/bcrypt"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -39,6 +40,7 @@ import (
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/publicshare/manager/registry"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
@@ -59,11 +61,13 @@ type config struct {
DbHost string `mapstructure:"db_host"`
DbPort int `mapstructure:"db_port"`
DbName string `mapstructure:"db_name"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
type manager struct {
c *config
db *sql.DB
c *config
db *sql.DB
client gatewayv1beta1.GatewayAPIClient
}
func (c *config) init() {
@@ -107,9 +111,15 @@ func New(m map[string]interface{}) (publicshare.Manager, error) {
return nil, err
}
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return nil, err
}
mgr := manager{
c: c,
db: db,
c: c,
db: db,
client: gw,
}
go mgr.startJanitorRun()
@@ -259,7 +269,11 @@ func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*
}
return nil, "", err
}
return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, "", err
}
return share, s.ShareWith, nil
}
func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) {
@@ -272,7 +286,11 @@ func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.U
}
return nil, "", err
}
return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, "", err
}
return share, s.ShareWith, nil
}
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
@@ -362,7 +380,10 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
continue
}
cs3Share := conversions.ConvertToCS3PublicShare(s)
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, err
}
if expired(cs3Share) {
_ = m.cleanupExpiredShares()
} else {
@@ -425,7 +446,10 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth
}
return nil, err
}
cs3Share := conversions.ConvertToCS3PublicShare(s)
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, err
}
if s.ShareWith != "" {
if !authenticate(cs3Share, s.ShareWith, auth) {
// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
+45 -11
View File
@@ -27,6 +27,7 @@ import (
"strings"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -34,6 +35,7 @@ import (
conversions "github.com/cs3org/reva/v2/pkg/cbox/utils"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/utils"
@@ -60,11 +62,13 @@ type config struct {
DbHost string `mapstructure:"db_host"`
DbPort int `mapstructure:"db_port"`
DbName string `mapstructure:"db_name"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
type mgr struct {
c *config
db *sql.DB
c *config
db *sql.DB
client gatewayv1beta1.GatewayAPIClient
}
// New returns a new share manager.
@@ -80,9 +84,15 @@ func New(m map[string]interface{}) (share.Manager, error) {
return nil, err
}
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return nil, err
}
return &mgr{
c: c,
db: db,
c: c,
db: db,
client: gw,
}, nil
}
@@ -175,7 +185,11 @@ func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collabor
}
return nil, err
}
return conversions.ConvertToCS3Share(s), nil
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
@@ -191,7 +205,11 @@ func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*colla
}
return nil, err
}
return conversions.ConvertToCS3Share(s), nil
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
@@ -313,7 +331,11 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) (
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
continue
}
shares = append(shares, conversions.ConvertToCS3Share(s))
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
continue
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
@@ -323,7 +345,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) (
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, _ *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := conversions.FormatUserID(user.Id)
@@ -365,7 +387,11 @@ func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.F
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
continue
}
shares = append(shares, conversions.ConvertToCS3ReceivedShare(s))
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
continue
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
@@ -400,7 +426,11 @@ func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*
}
return nil, err
}
return conversions.ConvertToCS3ReceivedShare(s), nil
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
@@ -431,7 +461,11 @@ func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey)
}
return nil, err
}
return conversions.ConvertToCS3ReceivedShare(s), nil
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
+139 -71
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,11 +19,14 @@
package utils
import (
"strings"
"context"
"errors"
"time"
gatewayv1beta1 "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"
rpcv1beta1 "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"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -33,24 +36,28 @@ import (
// DBShare stores information about user and public shares.
type DBShare struct {
ID string
UIDOwner string
UIDInitiator string
Prefix string
ItemSource string
ItemType string
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
State int
ID string
UIDOwner string
UIDInitiator string
Prefix string
ItemSource string
ItemType string
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
State int
Quicklink bool
Description string
NotifyUploads bool
NotifyUploadsExtraRecipients string
}
// FormatGrantee formats a CS3API grantee to a string
// FormatGrantee formats a CS3API grantee to a string.
func FormatGrantee(g *provider.Grantee) (int, string) {
var granteeType int
var formattedID string
@@ -67,23 +74,31 @@ func FormatGrantee(g *provider.Grantee) (int, string) {
return granteeType, formattedID
}
// ExtractGrantee retrieves the CS3API grantee from a formatted string
func ExtractGrantee(t int, g string) *provider.Grantee {
// ExtractGrantee retrieves the CS3API grantee from a formatted string.
func ExtractGrantee(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, t int, g string) (*provider.Grantee, error) {
var grantee provider.Grantee
switch t {
case 0:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
grantee.Id = &provider.Grantee_UserId{UserId: ExtractUserID(g)}
user, err := ExtractUserID(ctx, gateway, g)
if err != nil {
return nil, err
}
grantee.Id = &provider.Grantee_UserId{UserId: user}
case 1:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
grantee.Id = &provider.Grantee_GroupId{GroupId: ExtractGroupID(g)}
group, err := ExtractGroupID(ctx, gateway, g)
if err != nil {
return nil, err
}
grantee.Id = &provider.Grantee_GroupId{GroupId: group}
default:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
}
return &grantee
return &grantee, nil
}
// ResourceTypeToItem maps a resource type to a string
// ResourceTypeToItem maps a resource type to a string.
func ResourceTypeToItem(r provider.ResourceType) string {
switch r {
case provider.ResourceType_RESOURCE_TYPE_FILE:
@@ -99,7 +114,7 @@ func ResourceTypeToItem(r provider.ResourceType) string {
}
}
// ResourceTypeToItemInt maps a resource type to an integer
// ResourceTypeToItemInt maps a resource type to an integer.
func ResourceTypeToItemInt(r provider.ResourceType) int {
switch r {
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
@@ -111,7 +126,7 @@ func ResourceTypeToItemInt(r provider.ResourceType) int {
}
}
// SharePermToInt maps read/write permissions to an integer
// SharePermToInt maps read/write permissions to an integer.
func SharePermToInt(p *provider.ResourcePermissions) int {
var perm int
switch {
@@ -126,13 +141,25 @@ func SharePermToInt(p *provider.ResourcePermissions) int {
return perm
}
// IntTosharePerm retrieves read/write permissions from an integer
// IntTosharePerm retrieves read/write permissions from an integer.
func IntTosharePerm(p int, itemType string) *provider.ResourcePermissions {
perms, _ := conversions.NewPermissions(p)
return conversions.RoleFromOCSPermissions(perms).CS3ResourcePermissions()
switch p {
case 1:
return conversions.NewViewerRole(false).CS3ResourcePermissions()
case 15:
if itemType == "folder" {
return conversions.NewEditorRole(false).CS3ResourcePermissions()
}
return conversions.NewFileEditorRole().CS3ResourcePermissions()
case 4:
return conversions.NewUploaderRole().CS3ResourcePermissions()
default:
// TODO we may have other options, for now this is a denial
return &provider.ResourcePermissions{}
}
}
// IntToShareState retrieves the received share state from an integer
// IntToShareState retrieves the received share state from an integer.
func IntToShareState(g int) collaboration.ShareState {
switch g {
case 0:
@@ -146,66 +173,95 @@ func IntToShareState(g int) collaboration.ShareState {
}
}
// FormatUserID formats a CS3API user ID to a string
// FormatUserID formats a CS3API user ID to a string.
func FormatUserID(u *userpb.UserId) string {
return u.OpaqueId
}
// ExtractUserID retrieves a CS3API user ID from a string
func ExtractUserID(u string) *userpb.UserId {
t := userpb.UserType_USER_TYPE_PRIMARY
if strings.HasPrefix(u, "guest:") {
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
} else if strings.Contains(u, "@") {
t = userpb.UserType_USER_TYPE_FEDERATED
// ExtractUserID retrieves a CS3API user ID from a string.
func ExtractUserID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*userpb.UserId, error) {
userRes, err := gateway.GetUser(ctx, &userpb.GetUserRequest{
UserId: &userpb.UserId{OpaqueId: u},
})
if err != nil {
return nil, err
}
return &userpb.UserId{OpaqueId: u, Type: t}
if userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(userRes.Status.Message)
}
return userRes.User.Id, nil
}
// FormatGroupID formats a CS3API group ID to a string
// FormatGroupID formats a CS3API group ID to a string.
func FormatGroupID(u *grouppb.GroupId) string {
return u.OpaqueId
}
// ExtractGroupID retrieves a CS3API group ID from a string
func ExtractGroupID(u string) *grouppb.GroupId {
return &grouppb.GroupId{OpaqueId: u}
// ExtractGroupID retrieves a CS3API group ID from a string.
func ExtractGroupID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*grouppb.GroupId, error) {
groupRes, err := gateway.GetGroup(ctx, &grouppb.GetGroupRequest{
GroupId: &grouppb.GroupId{OpaqueId: u},
})
if err != nil {
return nil, err
}
if groupRes.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(groupRes.Status.Message)
}
return groupRes.Group.Id, nil
}
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share
func ConvertToCS3Share(s DBShare) *collaboration.Share {
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share.
func ConvertToCS3Share(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.Share, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
if err != nil {
return nil, err
}
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
if err != nil {
return nil, err
}
grantee, err := ExtractGrantee(ctx, gateway, s.ShareType, s.ShareWith)
if err != nil {
return nil, err
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: s.ID,
},
//ResourceId: &provider.Reference{StorageId: s.Prefix, NodeId: s.ItemSource},
ResourceId: &provider.ResourceId{
SpaceId: s.Prefix,
OpaqueId: s.ItemSource,
StorageId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &collaboration.SharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Grantee: ExtractGrantee(s.ShareType, s.ShareWith),
Owner: ExtractUserID(s.UIDOwner),
Creator: ExtractUserID(s.UIDInitiator),
Grantee: grantee,
Owner: owner,
Creator: creator,
Ctime: ts,
Mtime: ts,
}
}, nil
}
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share
func ConvertToCS3ReceivedShare(s DBShare) *collaboration.ReceivedShare {
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share.
func ConvertToCS3ReceivedShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.ReceivedShare, error) {
share, err := ConvertToCS3Share(ctx, gateway, s)
if err != nil {
return nil, err
}
return &collaboration.ReceivedShare{
Share: ConvertToCS3Share(s),
State: IntToShareState(s.State),
MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")},
}
Share: share,
State: IntToShareState(s.State),
}, nil
}
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
func ConvertToCS3PublicShare(s DBShare) *link.PublicShare {
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share.
func ConvertToCS3PublicShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*link.PublicShare, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
@@ -222,22 +278,34 @@ func ConvertToCS3PublicShare(s DBShare) *link.PublicShare {
}
}
}
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
if err != nil {
return nil, err
}
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
if err != nil {
return nil, err
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
SpaceId: s.Prefix,
OpaqueId: s.ItemSource,
StorageId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Owner: ExtractUserID(s.UIDOwner),
Creator: ExtractUserID(s.UIDInitiator),
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
}
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Owner: owner,
Creator: creator,
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
Quicklink: s.Quicklink,
Description: s.Description,
NotifyUploads: s.NotifyUploads,
NotifyUploadsExtraRecipients: s.NotifyUploadsExtraRecipients,
}, nil
}
+7
View File
@@ -173,6 +173,13 @@ func SharesNamespace(val string) Option {
}
}
// OCMNamespace provides a function to set the OCMNamespace config option.
func OCMNamespace(val string) Option {
return func(o *Options) {
o.config.OCMNamespace = val
}
}
// GatewaySvc provides a function to set the GatewaySvc config option.
func GatewaySvc(val string) Option {
return func(o *Options) {
+280
View File
@@ -0,0 +1,280 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package client
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"time"
"github.com/cs3org/reva/v2/internal/http/services/ocmd"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/pkg/errors"
)
// ErrTokenInvalid is the error returned by the invite-accepted
// endpoint when the token is not valid.
var ErrTokenInvalid = errors.New("the invitation token is invalid")
// ErrServiceNotTrusted is the error returned by the invite-accepted
// endpoint when the service is not trusted to accept invitations.
var ErrServiceNotTrusted = errors.New("service is not trusted to accept invitations")
// ErrUserAlreadyAccepted is the error returned by the invite-accepted
// endpoint when a user is already know by the remote cloud.
var ErrUserAlreadyAccepted = errors.New("user already accepted an invitation token")
// ErrTokenNotFound is the error returned by the invite-accepted
// endpoint when the request is done using a not existing token.
var ErrTokenNotFound = errors.New("token not found")
// ErrInvalidParameters is the error returned by the shares endpoint
// when the request does not contain required properties.
var ErrInvalidParameters = errors.New("invalid parameters")
// OCMClient is the client for an OCM provider.
type OCMClient struct {
client *http.Client
}
// Config is the configuration to be used for the OCMClient.
type Config struct {
Timeout time.Duration
Insecure bool
}
// New returns a new OCMClient.
func New(c *Config) *OCMClient {
return &OCMClient{
client: rhttp.GetHTTPClient(
rhttp.Timeout(c.Timeout),
rhttp.Insecure(c.Insecure),
),
}
}
// InviteAcceptedRequest contains the parameters for accepting
// an invitation.
type InviteAcceptedRequest struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
RecipientProvider string `json:"recipientProvider"`
Token string `json:"token"`
}
// User contains the remote user's information when accepting
// an invitation.
type User struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}
func (r *InviteAcceptedRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}
// InviteAccepted informs the sender that the invitation was accepted to start sharing
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *InviteAcceptedRequest) (*User, error) {
url, err := url.JoinPath(endpoint, "invite-accepted")
if err != nil {
return nil, err
}
body, err := r.toJSON()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
return c.parseInviteAcceptedResponse(resp)
}
func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) {
switch r.StatusCode {
case http.StatusOK:
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return &u, nil
case http.StatusBadRequest:
return nil, ErrTokenInvalid
case http.StatusNotFound:
return nil, ErrTokenNotFound
case http.StatusConflict:
return nil, ErrUserAlreadyAccepted
case http.StatusForbidden:
return nil, ErrServiceNotTrusted
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return nil, errtypes.InternalError(string(body))
}
// NewShareRequest contains the parameters for creating a new OCM share.
type NewShareRequest struct {
ShareWith string `json:"shareWith"`
Name string `json:"name"`
Description string `json:"description"`
ProviderID string `json:"providerId"`
Owner string `json:"owner"`
Sender string `json:"sender"`
OwnerDisplayName string `json:"ownerDisplayName"`
SenderDisplayName string `json:"senderDisplayName"`
ShareType string `json:"shareType"`
Expiration uint64 `json:"expiration"`
ResourceType string `json:"resourceType"`
Protocols ocmd.Protocols `json:"protocol"`
}
func (r *NewShareRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}
// NewShareResponse is the response returned when creating a new share.
type NewShareResponse struct {
RecipientDisplayName string `json:"recipientDisplayName"`
}
// NewShare creates a new share.
// https://github.com/cs3org/OCM-API/blob/develop/spec.yaml
func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRequest) (*NewShareResponse, error) {
url, err := url.JoinPath(endpoint, "shares")
if err != nil {
return nil, err
}
body, err := r.toJSON()
if err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
log.Debug().Msgf("Sending OCM /shares POST to %s: %s", url, body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
return c.parseNewShareResponse(resp)
}
func (c *OCMClient) parseNewShareResponse(r *http.Response) (*NewShareResponse, error) {
switch r.StatusCode {
case http.StatusOK, http.StatusCreated:
var res NewShareResponse
err := json.NewDecoder(r.Body).Decode(&res)
return &res, err
case http.StatusBadRequest:
return nil, ErrInvalidParameters
case http.StatusUnauthorized, http.StatusForbidden:
return nil, ErrServiceNotTrusted
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return nil, errtypes.InternalError(string(body))
}
// Capabilities contains a set of properties exposed by
// a remote cloud storage.
type Capabilities struct {
Enabled bool `json:"enabled"`
APIVersion string `json:"apiVersion"`
EndPoint string `json:"endPoint"`
Provider string `json:"provider"`
ResourceTypes []struct {
Name string `json:"name"`
ShareTypes []string `json:"shareTypes"`
Protocols struct {
Webdav *string `json:"webdav"`
Webapp *string `json:"webapp"`
Datatx *string `json:"datatx"`
} `json:"protocols"`
} `json:"resourceTypes"`
Capabilities []string `json:"capabilities"`
}
// Discovery returns a number of properties used to discover the capabilities offered by a remote cloud storage.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get
func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabilities, error) {
url, err := url.JoinPath(endpoint, "shares")
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
var cap Capabilities
if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
return nil, err
}
return &cap, nil
}
+27 -14
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,26 +20,39 @@ package invite
import (
"context"
"errors"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
)
// Manager is the interface that is used to perform operations to invites.
type Manager interface {
// GenerateToken creates a new token for the user with a specified validity.
GenerateToken(ctx context.Context) (*invitepb.InviteToken, error)
// Repository is the interfaces used to store the tokens and the invited users.
type Repository interface {
// AddToken stores the token in the repository.
AddToken(ctx context.Context, token *invitepb.InviteToken) error
// ForwardInvite forwards a received invite to the sync'n'share system provider.
ForwardInvite(ctx context.Context, invite *invitepb.InviteToken, originProvider *ocmprovider.ProviderInfo) error
// GetToken gets the token from the repository.
GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error)
// AcceptInvite completes an invitation acceptance.
AcceptInvite(ctx context.Context, invite *invitepb.InviteToken, remoteUser *userpb.User) error
// ListTokens gets the valid tokens from the repository (i.e. not expired).
ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error)
// GetAcceptedUser retrieves details about a remote user who has accepted an invite to share.
GetAcceptedUser(ctx context.Context, remoteUserID *userpb.UserId) (*userpb.User, error)
// AddRemoteUser stores the remote user.
AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error
// FindAcceptedUsers finds remote users who have accepted invites based on their attributes.
FindAcceptedUsers(ctx context.Context, query string) ([]*userpb.User, error)
// GetRemoteUser retrieves details about a remote user who has accepted an invite to share.
GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error)
// FindRemoteUsers finds remote users who have accepted invites based on their attributes.
FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error)
// DeleteRemoteUser removes from the remote user from the initiator's list.
DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error
}
// ErrTokenNotFound is the error returned when the token does not exist.
var ErrTokenNotFound = errors.New("token not found")
// ErrUserAlreadyAccepted is the error returned when the user was
// already added to the accepted users list.
var ErrUserAlreadyAccepted = errors.New("user already added to accepted users")
-325
View File
@@ -1,325 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package json
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/ocm/invite/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/invite/token"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
const acceptInviteEndpoint = "invites/accept"
type inviteModel struct {
File string
Invites map[string]*invitepb.InviteToken `json:"invites"`
AcceptedUsers map[string][]*userpb.User `json:"accepted_users"`
}
type manager struct {
config *config
sync.Mutex // concurrent access to the file
model *inviteModel
client *http.Client
}
type config struct {
File string `mapstructure:"file"`
Expiration string `mapstructure:"expiration"`
InsecureConnections bool `mapstructure:"insecure_connections"`
}
func init() {
registry.Register("json", New)
}
func (c *config) init() error {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-invites.json"
}
if c.Expiration == "" {
c.Expiration = token.DefaultExpirationTime
}
return nil
}
// New returns a new invite manager object.
func New(m map[string]interface{}) (invite.Manager, error) {
config, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "error parsing config for json invite manager")
return nil, err
}
err = config.init()
if err != nil {
err = errors.Wrap(err, "error setting config defaults for json invite manager")
return nil, err
}
// load or create file
model, err := loadOrCreate(config.File)
if err != nil {
err = errors.Wrap(err, "error loading the file containing the invites")
return nil, err
}
manager := &manager{
config: config,
model: model,
client: rhttp.GetHTTPClient(
rhttp.Timeout(5*time.Second),
rhttp.Insecure(config.InsecureConnections),
),
}
return manager, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func loadOrCreate(file string) (*inviteModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
err = errors.Wrap(err, "error creating the invite storage file: "+file)
return nil, err
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
err = errors.Wrap(err, "error opening the invite storage file: "+file)
return nil, err
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
err = errors.Wrap(err, "error reading the data")
return nil, err
}
model := &inviteModel{}
if err := json.Unmarshal(data, model); err != nil {
err = errors.Wrap(err, "error decoding invite data to json")
return nil, err
}
if model.Invites == nil {
model.Invites = make(map[string]*invitepb.InviteToken)
}
if model.AcceptedUsers == nil {
model.AcceptedUsers = make(map[string][]*userpb.User)
}
model.File = file
return model, nil
}
func (model *inviteModel) Save() error {
data, err := json.Marshal(model)
if err != nil {
err = errors.Wrap(err, "error encoding invite data to json")
return err
}
if err := os.WriteFile(model.File, data, 0644); err != nil {
err = errors.Wrap(err, "error writing invite data to file: "+model.File)
return err
}
return nil
}
func (m *manager) GenerateToken(ctx context.Context) (*invitepb.InviteToken, error) {
contexUser := ctxpkg.ContextMustGetUser(ctx)
inviteToken, err := token.CreateToken(m.config.Expiration, contexUser.GetId())
if err != nil {
return nil, err
}
// Store token data
m.Lock()
defer m.Unlock()
m.model.Invites[inviteToken.GetToken()] = inviteToken
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "error saving model")
return nil, err
}
return inviteToken, nil
}
func (m *manager) ForwardInvite(ctx context.Context, invite *invitepb.InviteToken, originProvider *ocmprovider.ProviderInfo) error {
contextUser := ctxpkg.ContextMustGetUser(ctx)
recipientProvider := contextUser.GetId().GetIdp()
requestBody := url.Values{
"token": {invite.GetToken()},
"userID": {contextUser.GetId().GetOpaqueId()},
"recipientProvider": {recipientProvider},
"email": {contextUser.GetMail()},
"name": {contextUser.GetDisplayName()},
}
ocmEndpoint, err := getOCMEndpoint(originProvider)
if err != nil {
return err
}
u, err := url.Parse(ocmEndpoint)
if err != nil {
return err
}
u.Path = path.Join(u.Path, acceptInviteEndpoint)
recipientURL := u.String()
req, err := http.NewRequest("POST", recipientURL, strings.NewReader(requestBody.Encode()))
if err != nil {
return errors.Wrap(err, "json: error framing post request")
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
resp, err := m.client.Do(req)
if err != nil {
err = errors.Wrap(err, "json: error sending post request")
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, e := io.ReadAll(resp.Body)
if e != nil {
return errors.Wrap(e, "json: error reading request body")
}
return errors.Wrap(fmt.Errorf("%s: %s", resp.Status, string(respBody)), "json: error sending accept post request")
}
return nil
}
func (m *manager) AcceptInvite(ctx context.Context, invite *invitepb.InviteToken, remoteUser *userpb.User) error {
m.Lock()
defer m.Unlock()
inviteToken, err := m.getTokenIfValid(invite)
if err != nil {
return err
}
currUser := inviteToken.GetUserId()
// do not allow the user who created the token to accept it
if remoteUser.Id.Idp == currUser.Idp && remoteUser.Id.OpaqueId == currUser.OpaqueId {
return errors.New("json: token creator and recipient are the same")
}
for _, acceptedUser := range m.model.AcceptedUsers[currUser.GetOpaqueId()] {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && acceptedUser.Id.GetIdp() == remoteUser.Id.Idp {
return errors.New("json: user already added to accepted users")
}
}
m.model.AcceptedUsers[currUser.GetOpaqueId()] = append(m.model.AcceptedUsers[currUser.GetOpaqueId()], remoteUser)
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "json: error saving model")
return err
}
return nil
}
func (m *manager) GetAcceptedUser(ctx context.Context, remoteUserID *userpb.UserId) (*userpb.User, error) {
userKey := ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()
for _, acceptedUser := range m.model.AcceptedUsers[userKey] {
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || acceptedUser.Id.GetIdp() == remoteUserID.Idp) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindAcceptedUsers(ctx context.Context, query string) ([]*userpb.User, error) {
users := []*userpb.User{}
userKey := ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()
for _, acceptedUser := range m.model.AcceptedUsers[userKey] {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) getTokenIfValid(token *invitepb.InviteToken) (*invitepb.InviteToken, error) {
inviteToken, ok := m.model.Invites[token.GetToken()]
if !ok {
return nil, errors.New("json: invalid token")
}
if uint64(time.Now().Unix()) > inviteToken.Expiration.Seconds {
return nil, errors.New("json: token expired")
}
return inviteToken, nil
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("json: ocm endpoint not specified for mesh provider")
}
@@ -1,233 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package memory
import (
"context"
"net/http"
"net/url"
"path"
"strings"
"sync"
"time"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/ocm/invite/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/invite/token"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
const acceptInviteEndpoint = "invites/accept"
func init() {
registry.Register("memory", New)
}
func (c *config) init() {
if c.Expiration == "" {
c.Expiration = token.DefaultExpirationTime
}
}
// New returns a new invite manager.
func New(m map[string]interface{}) (invite.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
c.init()
return &manager{
Invites: sync.Map{},
AcceptedUsers: sync.Map{},
Config: c,
Client: rhttp.GetHTTPClient(
rhttp.Timeout(5*time.Second),
rhttp.Insecure(c.InsecureConnections),
),
}, nil
}
type manager struct {
Invites sync.Map
AcceptedUsers sync.Map
Client *http.Client
Config *config
}
type config struct {
Expiration string `mapstructure:"expiration"`
InsecureConnections bool `mapstructure:"insecure_connections"`
}
func (m *manager) GenerateToken(ctx context.Context) (*invitepb.InviteToken, error) {
ctxUser := ctxpkg.ContextMustGetUser(ctx)
inviteToken, err := token.CreateToken(m.Config.Expiration, ctxUser.GetId())
if err != nil {
return nil, errors.Wrap(err, "memory: error creating token")
}
m.Invites.Store(inviteToken.GetToken(), inviteToken)
return inviteToken, nil
}
func (m *manager) ForwardInvite(ctx context.Context, invite *invitepb.InviteToken, originProvider *ocmprovider.ProviderInfo) error {
contextUser := ctxpkg.ContextMustGetUser(ctx)
requestBody := url.Values{
"token": {invite.GetToken()},
"userID": {contextUser.GetId().GetOpaqueId()},
"recipientProvider": {contextUser.GetId().GetIdp()},
"email": {contextUser.GetMail()},
"name": {contextUser.GetDisplayName()},
}
ocmEndpoint, err := getOCMEndpoint(originProvider)
if err != nil {
return err
}
u, err := url.Parse(ocmEndpoint)
if err != nil {
return err
}
u.Path = path.Join(u.Path, acceptInviteEndpoint)
recipientURL := u.String()
req, err := http.NewRequest("POST", recipientURL, strings.NewReader(requestBody.Encode()))
if err != nil {
return errors.Wrap(err, "json: error framing post request")
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
resp, err := m.Client.Do(req)
if err != nil {
err = errors.Wrap(err, "memory: error sending post request")
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = errors.Wrap(errors.New(resp.Status), "memory: error sending accept post request")
return err
}
return nil
}
func (m *manager) AcceptInvite(ctx context.Context, invite *invitepb.InviteToken, remoteUser *userpb.User) error {
inviteToken, err := m.getTokenIfValid(invite)
if err != nil {
return err
}
currUser := inviteToken.GetUserId()
// do not allow the user who created the token to accept it
if remoteUser.Id.Idp == currUser.Idp && remoteUser.Id.OpaqueId == currUser.OpaqueId {
return errors.New("memory: token creator and recipient are the same")
}
usersList, ok := m.AcceptedUsers.Load(currUser)
acceptedUsers := usersList.([]*userpb.User)
if ok {
for _, acceptedUser := range acceptedUsers {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && acceptedUser.Id.GetIdp() == remoteUser.Id.Idp {
return errors.New("memory: user already added to accepted users")
}
}
acceptedUsers = append(acceptedUsers, remoteUser)
m.AcceptedUsers.Store(currUser.GetOpaqueId(), acceptedUsers)
} else {
acceptedUsers := []*userpb.User{remoteUser}
m.AcceptedUsers.Store(currUser.GetOpaqueId(), acceptedUsers)
}
return nil
}
func (m *manager) GetAcceptedUser(ctx context.Context, remoteUserID *userpb.UserId) (*userpb.User, error) {
currUser := ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()
usersList, ok := m.AcceptedUsers.Load(currUser)
if !ok {
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || acceptedUser.Id.GetIdp() == remoteUserID.Idp) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindAcceptedUsers(ctx context.Context, query string) ([]*userpb.User, error) {
currUser := ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()
usersList, ok := m.AcceptedUsers.Load(currUser)
if !ok {
return []*userpb.User{}, nil
}
users := []*userpb.User{}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) getTokenIfValid(token *invitepb.InviteToken) (*invitepb.InviteToken, error) {
tokenInterface, ok := m.Invites.Load(token.GetToken())
if !ok {
return nil, errors.New("memory: invalid token")
}
inviteToken := tokenInterface.(*invitepb.InviteToken)
if uint64(time.Now().Unix()) > inviteToken.Expiration.Seconds {
return nil, errors.New("memory: token expired")
}
return inviteToken, nil
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("json: ocm endpoint not specified for mesh provider")
}
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package json
import (
"context"
"encoding/json"
"io"
"os"
"strings"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/cs3org/reva/v2/pkg/utils/list"
"github.com/pkg/errors"
)
type inviteModel struct {
File string
Invites map[string]*invitepb.InviteToken `json:"invites"`
AcceptedUsers map[string][]*userpb.User `json:"accepted_users"`
}
type manager struct {
config *config
sync.RWMutex // concurrent access to the file
model *inviteModel
}
type config struct {
File string `mapstructure:"file"`
}
func init() {
registry.Register("json", New)
}
func (c *config) ApplyDefaults() {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-invites.json"
}
}
// New returns a new invite manager object.
func New(m map[string]interface{}) (invite.Repository, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
// load or create file
model, err := loadOrCreate(c.File)
if err != nil {
return nil, errors.Wrap(err, "error loading the file containing the invites")
}
manager := &manager{
config: &c,
model: model,
}
return manager, nil
}
func loadOrCreate(file string) (*inviteModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the invite storage file: "+file)
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
return nil, errors.Wrap(err, "error opening the invite storage file: "+file)
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
return nil, errors.Wrap(err, "error reading the data")
}
model := &inviteModel{}
if err := json.Unmarshal(data, model); err != nil {
return nil, errors.Wrap(err, "error decoding invite data to json")
}
if model.Invites == nil {
model.Invites = make(map[string]*invitepb.InviteToken)
}
if model.AcceptedUsers == nil {
model.AcceptedUsers = make(map[string][]*userpb.User)
}
model.File = file
return model, nil
}
func (model *inviteModel) save() error {
data, err := json.Marshal(model)
if err != nil {
return errors.Wrap(err, "error encoding invite data to json")
}
if err := os.WriteFile(model.File, data, 0644); err != nil {
return errors.Wrap(err, "error writing invite data to file: "+model.File)
}
return nil
}
func (m *manager) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
m.Lock()
defer m.Unlock()
m.model.Invites[token.GetToken()] = token
if err := m.model.save(); err != nil {
return errors.Wrap(err, "json: error saving model")
}
return nil
}
func (m *manager) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
m.RLock()
defer m.RUnlock()
if tkn, ok := m.model.Invites[token]; ok {
return tkn, nil
}
return nil, invite.ErrTokenNotFound
}
func (m *manager) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
m.RLock()
defer m.RUnlock()
tokens := []*invitepb.InviteToken{}
for _, token := range m.model.Invites {
if utils.UserEqual(token.UserId, initiator) && !tokenIsExpired(token) {
tokens = append(tokens, token)
}
}
return tokens, nil
}
func tokenIsExpired(token *invitepb.InviteToken) bool {
return token.Expiration != nil && token.Expiration.Seconds > uint64(time.Now().Unix())
}
func (m *manager) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
m.Lock()
defer m.Unlock()
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && acceptedUser.Id.GetIdp() == remoteUser.Id.Idp {
return invite.ErrUserAlreadyAccepted
}
}
m.model.AcceptedUsers[initiator.GetOpaqueId()] = append(m.model.AcceptedUsers[initiator.GetOpaqueId()], remoteUser)
if err := m.model.save(); err != nil {
return errors.Wrap(err, "json: error saving model")
}
return nil
}
func (m *manager) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
m.RLock()
defer m.RUnlock()
log := appctx.GetLogger(ctx)
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
log.Info().Msgf("looking for '%s' at '%s' - considering '%s' at '%s'",
remoteUserID.OpaqueId,
remoteUserID.Idp,
acceptedUser.Id.GetOpaqueId(),
acceptedUser.Id.GetIdp(),
)
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || acceptedUser.Id.GetIdp() == remoteUserID.Idp) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error) {
m.RLock()
defer m.RUnlock()
users := []*userpb.User{}
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
m.Lock()
defer m.Unlock()
acceptedUsers, ok := m.model.AcceptedUsers[initiator.GetOpaqueId()]
if !ok {
return nil
}
for i, user := range acceptedUsers {
if (user.Id.GetOpaqueId() == remoteUser.OpaqueId) && (remoteUser.Idp == "" || user.Id.GetIdp() == remoteUser.Idp) {
acceptedUsers = list.Remove(acceptedUsers, i)
m.model.AcceptedUsers[initiator.GetOpaqueId()] = acceptedUsers
_ = m.model.save()
return nil
}
}
return nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,7 +20,8 @@ package loader
import (
// Load core share manager drivers.
_ "github.com/cs3org/reva/v2/pkg/ocm/share/manager/json"
_ "github.com/cs3org/reva/v2/pkg/ocm/share/manager/nextcloud"
// Add your own here
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/json"
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/memory"
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/sql"
// Add your own here.
)
@@ -0,0 +1,146 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package memory
import (
"context"
"strings"
"sync"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/list"
)
func init() {
registry.Register("memory", New)
}
// New returns a new invite manager.
func New(m map[string]interface{}) (invite.Repository, error) {
return &manager{
Invites: sync.Map{},
AcceptedUsers: sync.Map{},
}, nil
}
type manager struct {
Invites sync.Map
AcceptedUsers sync.Map
}
func (m *manager) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
m.Invites.Store(token.GetToken(), token)
return nil
}
func (m *manager) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
if v, ok := m.Invites.Load(token); ok {
return v.(*invitepb.InviteToken), nil
}
return nil, invite.ErrTokenNotFound
}
func (m *manager) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
tokens := []*invitepb.InviteToken{}
m.Invites.Range(func(_, value any) bool {
token := value.(*invitepb.InviteToken)
if utils.UserEqual(token.UserId, initiator) {
tokens = append(tokens, token)
}
return true
})
return tokens, nil
}
func (m *manager) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
usersList, ok := m.AcceptedUsers.Load(initiator)
acceptedUsers := usersList.([]*userpb.User)
if ok {
for _, acceptedUser := range acceptedUsers {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && acceptedUser.Id.GetIdp() == remoteUser.Id.Idp {
return invite.ErrUserAlreadyAccepted
}
}
acceptedUsers = append(acceptedUsers, remoteUser)
m.AcceptedUsers.Store(initiator.GetOpaqueId(), acceptedUsers)
} else {
acceptedUsers := []*userpb.User{remoteUser}
m.AcceptedUsers.Store(initiator.GetOpaqueId(), acceptedUsers)
}
return nil
}
func (m *manager) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || acceptedUser.Id.GetIdp() == remoteUserID.Idp) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error) {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return []*userpb.User{}, nil
}
users := []*userpb.User{}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return nil
}
acceptedUsers := usersList.([]*userpb.User)
for i, user := range acceptedUsers {
if (user.Id.GetOpaqueId() == remoteUser.OpaqueId) && (remoteUser.Idp == "" || user.Id.GetIdp() == remoteUser.Idp) {
m.AcceptedUsers.Store(initiator, list.Remove(acceptedUsers, i))
return nil
}
}
return nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -18,16 +18,18 @@
package registry
import "github.com/cs3org/reva/v2/pkg/ocm/share"
import (
"github.com/cs3org/reva/v2/pkg/ocm/invite"
)
// NewFunc is the function that share managers
// NewFunc is the function that invite repositories
// should register at init time.
type NewFunc func(map[string]interface{}) (share.Manager, error)
type NewFunc func(map[string]interface{}) (invite.Repository, error)
// NewFuncs is a map containing all the registered share managers.
// NewFuncs is a map containing all the registered invite repositories.
var NewFuncs = map[string]NewFunc{}
// Register registers a new share manager new function.
// Register registers a new invite repository new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
+251
View File
@@ -0,0 +1,251 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sql
import (
"context"
"database/sql"
"fmt"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
conversions "github.com/cs3org/reva/v2/pkg/cbox/utils"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/invite"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/go-sql-driver/mysql"
"github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
)
// This module implement the invite.Repository interface as a mysql driver.
//
// The OCM Invitation tokens are saved in the table:
// ocm_tokens(*token*, initiator, expiration, description)
//
// The OCM remote user are saved in the table:
// ocm_remote_users(*initiator*, *opaque_user_id*, *idp*, email, display_name)
func init() {
registry.Register("sql", New)
}
type mgr struct {
c *config
db *sql.DB
client gatewayv1beta1.GatewayAPIClient
}
type config struct {
DBUsername string `mapstructure:"db_username"`
DBPassword string `mapstructure:"db_password"`
DBAddress string `mapstructure:"db_address"`
DBName string `mapstructure:"db_name"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
func (c *config) ApplyDefaults() {
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
// New creates a sql repository for ocm tokens and users.
func New(m map[string]interface{}) (invite.Repository, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", c.DBUsername, c.DBPassword, c.DBAddress, c.DBName))
if err != nil {
return nil, errors.Wrap(err, "sql: error opening connection to mysql database")
}
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return nil, err
}
mgr := mgr{
c: &c,
db: db,
client: gw,
}
return &mgr, nil
}
// AddToken stores the token in the repository.
func (m *mgr) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
query := "INSERT INTO ocm_tokens SET token=?,initiator=?,expiration=?,description=?"
_, err := m.db.ExecContext(ctx, query, token.Token, conversions.FormatUserID(token.UserId), timestampToTime(token.Expiration), token.Description)
return err
}
func timestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.Seconds), int64(t.Nanos))
}
type dbToken struct {
Token string
Initiator string
Expiration time.Time
Description string
}
// GetToken gets the token from the repository.
func (m *mgr) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
query := "SELECT token, initiator, expiration, description FROM ocm_tokens where token=?"
var tkn dbToken
if err := m.db.QueryRowContext(ctx, query, token).Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, invite.ErrTokenNotFound
}
return nil, err
}
return m.convertToInviteToken(ctx, tkn)
}
func (m *mgr) convertToInviteToken(ctx context.Context, tkn dbToken) (*invitepb.InviteToken, error) {
user, err := conversions.ExtractUserID(ctx, m.client, tkn.Initiator)
if err != nil {
return nil, err
}
return &invitepb.InviteToken{
Token: tkn.Token,
UserId: user,
Expiration: &types.Timestamp{
Seconds: uint64(tkn.Expiration.Unix()),
},
Description: tkn.Description,
}, nil
}
func (m *mgr) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
query := "SELECT token, initiator, expiration, description FROM ocm_tokens WHERE initiator=? AND expiration > NOW()"
tokens := []*invitepb.InviteToken{}
rows, err := m.db.QueryContext(ctx, query, conversions.FormatUserID(initiator))
if err != nil {
return nil, err
}
var tkn dbToken
for rows.Next() {
if err := rows.Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
continue
}
token, err := m.convertToInviteToken(ctx, tkn)
if err != nil {
return nil, err
}
tokens = append(tokens, token)
}
return tokens, nil
}
// AddRemoteUser stores the remote user.
func (m *mgr) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
query := "INSERT INTO ocm_remote_users SET initiator=?, opaque_user_id=?, idp=?, email=?, display_name=?"
if _, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser.Id), remoteUser.Id.Idp, remoteUser.Mail, remoteUser.DisplayName); err != nil {
// check if the user already exist in the db
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry
var e *mysql.MySQLError
if errors.As(err, &e) && e.Number == 1062 {
return invite.ErrUserAlreadyAccepted
}
return err
}
return nil
}
type dbOCMUser struct {
OpaqueUserID string
Idp string
Email string
DisplayName string
}
// GetRemoteUser retrieves details about a remote user who has accepted an invite to share.
func (m *mgr) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
var user dbOCMUser
if err := m.db.QueryRowContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUserID), remoteUserID.Idp).
Scan(&user.OpaqueUserID, &user.Idp, &user.Email, &user.DisplayName); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
return nil, err
}
return user.toCS3User(), nil
}
func (u *dbOCMUser) toCS3User() *userpb.User {
return &userpb.User{
Id: &userpb.UserId{
Idp: u.Idp,
OpaqueId: u.OpaqueUserID,
Type: userpb.UserType_USER_TYPE_FEDERATED,
},
Mail: u.Email,
DisplayName: u.DisplayName,
}
}
// FindRemoteUsers finds remote users who have accepted invites based on their attributes.
func (m *mgr) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, attr string) ([]*userpb.User, error) {
// TODO: (gdelmont) this query can get really slow in case the number of rows is too high.
// For the time being this is not expected, but if in future this happens, consider to add
// a fulltext index.
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND (opaque_user_id LIKE ? OR idp LIKE ? OR email LIKE ? OR display_name LIKE ?)"
s := "%" + attr + "%"
params := []any{conversions.FormatUserID(initiator), s, s, s, s}
rows, err := m.db.QueryContext(ctx, query, params...)
if err != nil {
return nil, err
}
var u dbOCMUser
var users []*userpb.User
for rows.Next() {
if err := rows.Scan(&u.OpaqueUserID, &u.Idp, &u.Email, &u.DisplayName); err != nil {
continue
}
users = append(users, u.toCS3User())
}
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
func (m *mgr) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
query := "DELETE FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
_, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser), remoteUser.Idp)
return err
}
+28 -26
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -28,10 +28,11 @@ import (
"sync"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/provider"
"github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/mitchellh/mapstructure"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
)
@@ -41,26 +42,27 @@ func init() {
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
c.init()
providers := []*ocmprovider.ProviderInfo{}
f, err := os.ReadFile(c.Providers)
if err != nil {
return nil, err
}
providers := []*ocmprovider.ProviderInfo{}
err = json.Unmarshal(f, &providers)
if err != nil {
return nil, err
if !os.IsNotExist(err) {
return nil, err
}
} else {
err = json.Unmarshal(f, &providers)
if err != nil {
return nil, err
}
}
a := &authorizer{
providerIPs: sync.Map{},
conf: c,
conf: &c,
}
a.providers = a.getOCMProviders(providers)
@@ -72,7 +74,7 @@ type config struct {
VerifyRequestHostname bool `mapstructure:"verify_request_hostname"`
}
func (c *config) init() {
func (c *config) ApplyTemplates() {
if c.Providers == "" {
c.Providers = "/etc/revad/ocm-providers.json"
}
@@ -113,9 +115,10 @@ func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmpr
return nil, errtypes.NotFound(domain)
}
func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovider.ProviderInfo) error {
func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.ProviderInfo) error {
log := appctx.GetLogger(ctx)
var err error
normalizedDomain, err := normalizeDomain(provider.Domain)
normalizedDomain, err := normalizeDomain(pi.Domain)
if err != nil {
return err
}
@@ -133,20 +136,22 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide
switch {
case !providerAuthorized:
return errtypes.NotFound(provider.GetDomain())
return errtypes.NotFound(pi.GetDomain())
case !a.conf.VerifyRequestHostname:
return nil
case len(provider.Services) == 0:
case len(pi.Services) == 0:
return errtypes.NotSupported("No IP provided")
}
var ocmHost string
for _, p := range a.providers {
log.Debug().Msgf("Comparing '%s' to '%s'", p.Domain, normalizedDomain)
if p.Domain == normalizedDomain {
ocmHost, err = a.getOCMHost(p)
if err != nil {
return err
}
break
}
}
if ocmHost == "" {
@@ -169,8 +174,9 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide
}
for _, ip := range ipList {
if ip == provider.Services[0].Host {
if ip == pi.Services[0].Host {
providerAuthorized = true
break
}
}
if !providerAuthorized {
@@ -194,14 +200,10 @@ func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po
return
}
func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range provider.Services {
func (a *authorizer) getOCMHost(pi *ocmprovider.ProviderInfo) (string, error) {
for _, s := range pi.Services {
if s.Endpoint.Type.Name == "OCM" {
ocmHost, err := url.Parse(s.Host)
if err != nil {
return "", errors.Wrap(err, "json: error parsing OCM host URL")
}
return ocmHost.Host, nil
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -23,5 +23,5 @@ import (
_ "github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/json"
_ "github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/mentix"
_ "github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/open"
// Add your own here
// Add your own here.
)
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -29,13 +29,12 @@ import (
"sync"
"time"
"github.com/cs3org/reva/v2/pkg/rhttp"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/provider"
"github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/mitchellh/mapstructure"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
)
@@ -43,7 +42,7 @@ func init() {
registry.Register("mentix", New)
}
// Client is a Mentix API client
// Client is a Mentix API client.
type Client struct {
BaseURL string
HTTPClient *http.Client
@@ -51,12 +50,10 @@ type Client struct {
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
c.init()
client := &Client{
BaseURL: c.URL,
@@ -70,7 +67,7 @@ func New(m map[string]interface{}) (provider.Authorizer, error) {
return &authorizer{
client: client,
providerIPs: sync.Map{},
conf: c,
conf: &c,
}, nil
}
@@ -79,10 +76,10 @@ type config struct {
Timeout int64 `mapstructure:"timeout"`
RefreshInterval int64 `mapstructure:"refresh"`
VerifyRequestHostname bool `mapstructure:"verify_request_hostname"`
Insecure bool `mapstructure:"insecure"`
Insecure bool `mapstructure:"insecure" docs:"false;Whether to skip certificate checks when sending requests."`
}
func (c *config) init() {
func (c *config) ApplyDefaults() {
if c.URL == "" {
c.URL = "http://localhost:9600/mentix/cs3"
}
@@ -96,12 +93,28 @@ type authorizer struct {
conf *config
}
func normalizeDomain(d string) (string, error) {
var urlString string
if strings.Contains(d, "://") {
urlString = d
} else {
urlString = "https://" + d
}
u, err := url.Parse(urlString)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
func (a *authorizer) fetchProviders() ([]*ocmprovider.ProviderInfo, error) {
if (a.providers != nil) && (time.Now().Unix() < a.providersExpiration) {
return a.providers, nil
}
req, err := http.NewRequest("GET", a.client.BaseURL, nil)
req, err := http.NewRequest(http.MethodGet, a.client.BaseURL, nil)
if err != nil {
return nil, err
}
@@ -111,7 +124,7 @@ func (a *authorizer) fetchProviders() ([]*ocmprovider.ProviderInfo, error) {
res, err := a.client.HTTPClient.Do(req)
if err != nil {
err = errors.Wrap(err,
fmt.Sprintf("error fetching provider list from: %s", a.client.BaseURL))
fmt.Sprintf("mentix: error fetching provider list from: %s", a.client.BaseURL))
return nil, err
}
@@ -130,29 +143,37 @@ func (a *authorizer) fetchProviders() ([]*ocmprovider.ProviderInfo, error) {
}
func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmprovider.ProviderInfo, error) {
providers, err := a.fetchProviders()
normalizedDomain, err := normalizeDomain(domain)
if err != nil {
return nil, err
}
providers, err := a.fetchProviders()
if err != nil {
return nil, err
}
for _, p := range providers {
if strings.Contains(p.Domain, domain) {
if strings.Contains(p.Domain, normalizedDomain) {
return p, nil
}
}
return nil, errtypes.NotFound(domain)
}
func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovider.ProviderInfo) error {
func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.ProviderInfo) error {
providers, err := a.fetchProviders()
if err != nil {
return err
}
normalizedDomain, err := normalizeDomain(pi.Domain)
if err != nil {
return err
}
var providerAuthorized bool
if provider.Domain != "" {
if normalizedDomain != "" {
for _, p := range providers {
if p.Domain == provider.Domain {
if p.Domain == normalizedDomain {
providerAuthorized = true
break
}
@@ -163,24 +184,27 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide
switch {
case !providerAuthorized:
return errtypes.NotFound(provider.GetDomain())
return errtypes.NotFound(pi.GetDomain())
case !a.conf.VerifyRequestHostname:
return nil
case len(provider.Services) == 0:
return errtypes.NotSupported("No IP provided")
case len(pi.Services) == 0:
return errtypes.NotSupported(
fmt.Sprintf("mentix: provider %s has no supported services", pi.GetDomain()))
}
var ocmHost string
for _, p := range providers {
if p.Domain == provider.Domain {
if p.Domain == normalizedDomain {
ocmHost, err = a.getOCMHost(p)
if err != nil {
return err
}
break
}
}
if ocmHost == "" {
return errtypes.InternalError("mentix: ocm host not specified for mesh provider")
return errtypes.NotSupported(
fmt.Sprintf("mentix: provider %s is missing OCM endpoint", pi.GetDomain()))
}
providerAuthorized = false
@@ -190,7 +214,8 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide
} else {
addr, err := net.LookupIP(ocmHost)
if err != nil {
return errors.Wrap(err, "json: error looking up client IP")
return errors.Wrap(err,
fmt.Sprintf("mentix: error looking up IPs for OCM endpoint %s", ocmHost))
}
for _, a := range addr {
ipList = append(ipList, a.String())
@@ -199,12 +224,16 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide
}
for _, ip := range ipList {
if ip == provider.Services[0].Host {
if ip == pi.Services[0].Host {
providerAuthorized = true
break
}
}
if !providerAuthorized {
return errtypes.NotFound("OCM Host")
return errtypes.BadRequest(
fmt.Sprintf(
"Invalid requesting OCM endpoint IP %s of provider %s",
pi.Services[0].Host, pi.GetDomain()))
}
return nil
@@ -231,11 +260,7 @@ func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po
func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range provider.Services {
if s.Endpoint.Type.Name == "OCM" {
ocmHost, err := url.Parse(s.Host)
if err != nil {
return "", errors.Wrap(err, "json: error parsing OCM host URL")
}
return ocmHost.Host, nil
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
+6 -14
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@ package open
import (
"context"
"encoding/json"
"net/url"
"os"
"strings"
@@ -29,8 +28,7 @@ import (
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/provider"
"github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
)
func init() {
@@ -39,12 +37,10 @@ func init() {
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
c.init()
f, err := os.ReadFile(c.Providers)
if err != nil {
@@ -67,7 +63,7 @@ type config struct {
Providers string `mapstructure:"providers"`
}
func (c *config) init() {
func (c *config) ApplyDefaults() {
if c.Providers == "" {
c.Providers = "/etc/revad/ocm-providers.json"
}
@@ -107,11 +103,7 @@ func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po
func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range provider.Services {
if s.Endpoint.Type.Name == "OCM" {
ocmHost, err := url.Parse(s.Host)
if err != nil {
return "", errors.Wrap(err, "json: error parsing OCM host URL")
}
return ocmHost.Host, nil
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@
package registry
import "github.com/cs3org/reva/v2/pkg/ocm/provider"
import (
"github.com/cs3org/reva/v2/pkg/ocm/provider"
)
// NewFunc is the function that provider authorizers
// should register at init time.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
-603
View File
@@ -1,603 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package json
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/share/sender"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
)
func init() {
registry.Register("json", New)
}
// New returns a new authorizer object.
func New(m map[string]interface{}) (share.Manager, error) {
c, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
c.init()
// load or create file
model, err := loadOrCreate(c.File)
if err != nil {
err = errors.Wrap(err, "error loading the file containing the shares")
return nil, err
}
mgr := &mgr{
c: c,
model: model,
}
return mgr, nil
}
func loadOrCreate(file string) (*shareModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
err = errors.Wrap(err, "error creating the file: "+file)
return nil, err
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
err = errors.Wrap(err, "error opening the file: "+file)
return nil, err
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
err = errors.Wrap(err, "error reading the data")
return nil, err
}
m := &shareModel{}
if err := json.Unmarshal(data, m); err != nil {
err = errors.Wrap(err, "error decoding data to json")
return nil, err
}
if m.Shares == nil {
m.Shares = map[string]interface{}{}
}
if m.ReceivedShares == nil {
m.ReceivedShares = map[string]interface{}{}
}
m.file = file
return m, nil
}
type shareModel struct {
file string
Shares map[string]interface{} `json:"shares"`
ReceivedShares map[string]interface{} `json:"received_shares"`
}
type config struct {
File string `mapstructure:"file"`
InsecureConnections bool `mapstructure:"insecure_connections"`
}
func (c *config) init() {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-shares.json"
}
}
type mgr struct {
c *config
sync.Mutex // concurrent access to the file
model *shareModel
}
func (m *shareModel) Save() error {
data, err := json.Marshal(m)
if err != nil {
err = errors.Wrap(err, "error encoding to json")
return err
}
if err := os.WriteFile(m.file, data, 0644); err != nil {
err = errors.Wrap(err, "error writing to file: "+m.file)
return err
}
return nil
}
func (m *shareModel) ReadFile() error {
data, err := os.ReadFile(m.file)
if err != nil {
err = errors.Wrap(err, "error reading the data")
return err
}
if err := json.Unmarshal(data, m); err != nil {
err = errors.Wrap(err, "error decoding data to json")
return err
}
return nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func genID() string {
return uuid.New().String()
}
// Called from both grpc CreateOCMShare for outgoing
// and http /ocm/shares for incoming
// pi is provider info
// pm is permissions
func (m *mgr) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string,
pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) {
id := genID()
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
// Since both OCMCore and OCMShareProvider use the same package, we distinguish
// between calls received from them on the basis of whether they provide info
// about the remote provider on which the share is to be created.
// If this info is provided, this call is on the owner's mesh provider and so
// we call the CreateOCMCoreShare method on the remote provider as well,
// else this is received from another provider and we only create a local share.
var isOwnersMeshProvider bool
if pi != nil {
isOwnersMeshProvider = true
}
var userID *userpb.UserId
if !isOwnersMeshProvider {
// Since this call is on the remote provider, the owner of the resource is expected to be specified.
if owner == nil {
return nil, errors.New("json: owner of resource not provided")
}
userID = owner
g.Grantee.Opaque = &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
"token": {
Decoder: "plain",
Value: []byte(token),
},
},
}
} else {
userID = ctxpkg.ContextMustGetUser(ctx).GetId()
}
// do not allow share to myself if share is for a user
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(g.Grantee.GetUserId(), userID) {
return nil, errors.New("json: user and grantee are the same")
}
// check if share already exists.
key := &ocm.ShareKey{
Owner: userID,
ResourceId: md,
Grantee: g.Grantee,
}
_, err := m.getByKey(ctx, key)
// share already exists
if isOwnersMeshProvider && err == nil {
return nil, errtypes.AlreadyExists(key.String())
}
s := &ocm.Share{
Id: &ocm.ShareId{
OpaqueId: id,
},
Name: name,
ResourceId: md,
Permissions: g.Permissions,
Grantee: g.Grantee,
Owner: userID,
Creator: userID,
Ctime: ts,
Mtime: ts,
ShareType: st,
}
if isOwnersMeshProvider {
protocol := map[string]interface{}{
"name": "webdav",
"options": map[string]string{
"permissions": pm,
"token": ctxpkg.ContextMustGetToken(ctx),
},
}
if st == ocm.Share_SHARE_TYPE_TRANSFER {
protocol["name"] = "datatx"
}
requestBodyMap := map[string]interface{}{
"shareWith": g.Grantee.GetUserId().OpaqueId,
"name": name,
"providerId": fmt.Sprintf("%s:%s", md.StorageId, md.OpaqueId),
"owner": userID.OpaqueId,
"protocol": protocol,
"meshProvider": userID.Idp, // FIXME: move this into the 'owner' string?
}
err = sender.Send(requestBodyMap, pi)
if err != nil {
err = errors.Wrap(err, "error sending OCM POST")
return nil, err
}
}
m.Lock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
if isOwnersMeshProvider {
encShare, err := utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, err
}
m.model.Shares[s.Id.OpaqueId] = string(encShare)
} else {
encShare, err := utils.MarshalProtoV1ToJSON(&ocm.ReceivedShare{
Share: s,
State: ocm.ShareState_SHARE_STATE_PENDING,
})
if err != nil {
return nil, err
}
m.model.ReceivedShares[s.Id.OpaqueId] = string(encShare)
}
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "error saving model")
return nil, err
}
m.Unlock()
return s, nil
}
func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
if s, ok := m.model.Shares[id.OpaqueId]; ok {
var share ocm.Share
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &share); err != nil {
return nil, err
}
return &share, nil
}
return nil, errtypes.NotFound(id.String())
}
func (m *mgr) getByKey(ctx context.Context, key *ocm.ShareKey) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
for _, s := range m.model.Shares {
var share ocm.Share
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &share); err != nil {
continue
}
if (utils.UserEqual(key.Owner, share.Owner) || utils.UserEqual(key.Owner, share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, share.ResourceId) && utils.GranteeEqual(key.Grantee, share.Grantee) {
return &share, nil
}
}
return nil, errtypes.NotFound(key.String())
}
func (m *mgr) get(ctx context.Context, ref *ocm.ShareReference) (s *ocm.Share, err error) {
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getByKey(ctx, ref.GetKey())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
// check if we are the owner
user := ctxpkg.ContextMustGetUser(ctx)
if utils.UserEqual(user.Id, s.Owner) || utils.UserEqual(user.Id, s.Creator) {
return s, nil
}
// we return not found to not disclose information
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) {
share, err := m.get(ctx, ref)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) Unshare(ctx context.Context, ref *ocm.ShareReference) error {
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return err
}
user := ctxpkg.ContextMustGetUser(ctx)
for id, s := range m.model.Shares {
var share ocm.Share
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &share); err != nil {
continue
}
if sharesEqual(ref, &share) {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
delete(m.model.Shares, id)
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "error saving model")
return err
}
return nil
}
}
}
return errtypes.NotFound(ref.String())
}
func sharesEqual(ref *ocm.ShareReference, s *ocm.Share) bool {
if ref.GetId() != nil && s.Id != nil {
if ref.GetId().OpaqueId == s.Id.OpaqueId {
return true
}
} else if ref.GetKey() != nil {
if (utils.UserEqual(ref.GetKey().Owner, s.Owner) || utils.UserEqual(ref.GetKey().Owner, s.Creator)) &&
utils.ResourceIDEqual(ref.GetKey().ResourceId, s.ResourceId) && utils.GranteeEqual(ref.GetKey().Grantee, s.Grantee) {
return true
}
}
return false
}
func (m *mgr) UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
for id, s := range m.model.Shares {
var share ocm.Share
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &share); err != nil {
continue
}
if sharesEqual(ref, &share) {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
now := time.Now().UnixNano()
share.Permissions = p
share.Mtime = &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
encShare, err := utils.MarshalProtoV1ToJSON(&share)
if err != nil {
return nil, err
}
m.model.Shares[id] = string(encShare)
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "error saving model")
return nil, err
}
return &share, nil
}
}
}
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
var ss []*ocm.Share
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
for _, s := range m.model.Shares {
var share ocm.Share
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &share); err != nil {
continue
}
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
// no filter we return earlier
if len(filters) == 0 {
ss = append(ss, &share)
} else {
// check filters
// TODO(labkode): add the rest of filters.
for _, f := range filters {
if f.Type == ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID {
if utils.ResourceIDEqual(share.ResourceId, f.GetResourceId()) {
ss = append(ss, &share)
}
}
}
}
}
}
return ss, nil
}
func (m *mgr) ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) {
var rss []*ocm.ReceivedShare
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
for _, s := range m.model.ReceivedShares {
var rs ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &rs); err != nil {
continue
}
share := rs.Share
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
// omit shares created by me
continue
}
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
rss = append(rss, &rs)
}
}
return rss, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
return m.getReceived(ctx, ref)
}
func (m *mgr) getReceived(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
for _, s := range m.model.ReceivedShares {
var rs ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1([]byte(s.(string)), &rs); err != nil {
continue
}
share := rs.Share
if sharesEqual(ref, share) {
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
return &rs, nil
}
}
}
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
rs, err := m.getReceived(ctx, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Share.Id}})
if err != nil {
return nil, err
}
m.Lock()
defer m.Unlock()
for i := range fieldMask.Paths {
switch fieldMask.Paths[i] {
case "state":
rs.State = share.State
case "mount_point":
rs.MountPoint = share.MountPoint
default:
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
}
}
if err := m.model.ReadFile(); err != nil {
err = errors.Wrap(err, "error reading model")
return nil, err
}
encShare, err := utils.MarshalProtoV1ToJSON(rs)
if err != nil {
return nil, err
}
m.model.ReceivedShares[rs.Share.Id.GetOpaqueId()] = string(encShare)
if err := m.model.Save(); err != nil {
err = errors.Wrap(err, "error saving model")
return nil, err
}
return rs, nil
}
@@ -1,624 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
package nextcloud
import (
"context"
"encoding/json"
"io"
"math/rand"
"net/http"
"strings"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/utils"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/ocm/share/sender"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randSeq(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func init() {
registry.Register("nextcloud", New)
}
// Manager is the Nextcloud-based implementation of the share.Manager interface
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
type Manager struct {
client *http.Client
sharedSecret string
endPoint string
}
// ShareManagerConfig contains config for a Nextcloud-based ShareManager
type ShareManagerConfig struct {
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
SharedSecret string `mapstructure:"shared_secret"`
MockHTTP bool `mapstructure:"mock_http"`
}
// Action describes a REST request to forward to the Nextcloud backend
type Action struct {
verb string
argS string
}
// GranteeAltMap is an alternative map to JSON-unmarshal a Grantee
// Grantees are hard to unmarshal, so unmarshalling into a map[string]interface{} first,
// see also https://github.com/pondersource/sciencemesh-nextcloud/issues/27
type GranteeAltMap struct {
ID *provider.Grantee_UserId `json:"id"`
}
// ShareAltMap is an alternative map to JSON-unmarshal a Share
type ShareAltMap struct {
ID *ocm.ShareId `json:"id"`
ResourceID *provider.ResourceId `json:"resource_id"`
Permissions *ocm.SharePermissions `json:"permissions"`
Grantee *GranteeAltMap `json:"grantee"`
Owner *userpb.UserId `json:"owner"`
Creator *userpb.UserId `json:"creator"`
Ctime *typespb.Timestamp `json:"ctime"`
Mtime *typespb.Timestamp `json:"mtime"`
}
// ReceivedShareAltMap is an alternative map to JSON-unmarshal a ReceivedShare
type ReceivedShareAltMap struct {
Share *ShareAltMap `json:"share"`
State ocm.ShareState `json:"state"`
}
func (c *ShareManagerConfig) init() {
}
func parseConfig(m map[string]interface{}) (*ShareManagerConfig, error) {
c := &ShareManagerConfig{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
func getUser(ctx context.Context) (*userpb.User, error) {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx")
return nil, err
}
return u, nil
}
// New returns a share manager implementation that verifies against a Nextcloud backend.
func New(m map[string]interface{}) (share.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
return NewShareManager(c)
}
// NewShareManager returns a new Nextcloud-based ShareManager
func NewShareManager(c *ShareManagerConfig) (*Manager, error) {
var client *http.Client
if c.MockHTTP {
// called := make([]string, 0)
// nextcloudServerMock := GetNextcloudServerMock(&called)
// client, _ = TestingHTTPClient(nextcloudServerMock)
// Wait for SetHTTPClient to be called later
client = nil
} else {
if len(c.EndPoint) == 0 {
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.ocmshareprovider.drivers.nextcloud]' and '[grpc.services.ocmcore.drivers.nextcloud]'")
}
client = &http.Client{}
}
return &Manager{
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
sharedSecret: c.SharedSecret,
client: client,
}, nil
}
// SetHTTPClient sets the HTTP client
func (sm *Manager) SetHTTPClient(c *http.Client) {
sm.client = c
}
func getUsername(ctx context.Context) string {
user, err := getUser(ctx)
if err != nil {
return "unknown"
}
if len(user.Username) > 0 {
return user.Username
}
if len(user.Id.OpaqueId) > 0 {
return user.Id.OpaqueId
}
return "empty-username"
}
func (sm *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
url := sm.endPoint + "~" + username + "/api/ocm/" + a.verb
log := appctx.GetLogger(ctx)
log.Info().Msgf("am.do %s %s", url, a.argS)
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
if err != nil {
return 0, nil, err
}
req.Header.Set("X-Reva-Secret", sm.sharedSecret)
req.Header.Set("Content-Type", "application/json")
resp, err := sm.client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, err
}
// curl -i -H 'application/json' -H 'X-Reva-Secret: shared-secret-1' -d '{"md":{"opaque_id":"fileid-/other/q/as"},"g":{"grantee":{"type":1,"Id":{"UserId":{"idp":"revanc2.docker","opaque_id":"marie"}}},"permissions":{"permissions":{"get_path":true,"initiate_file_download":true,"list_container":true,"list_file_versions":true,"stat":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' https://nc1.docker/index.php/apps/sciencemesh/~/api/ocm/addSentShare
log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
return resp.StatusCode, body, nil
}
// Share is called from both grpc CreateOCMShare for outgoing
// and http /ocm/shares for incoming
// pi is provider info
// pm is permissions
func (sm *Manager) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string,
pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) {
// Since both OCMCore and OCMShareProvider use the same package, we distinguish
// between calls received from them on the basis of whether they provide info
// about the remote provider on which the share is to be created.
// If this info is provided, this call is on the owner's mesh provider and so
// we call the CreateOCMCoreShare method on the remote provider as well,
// else this is received from another provider and we only create a local share.
var isOwnersMeshProvider bool
var apiMethod string
var username string
if pi != nil {
isOwnersMeshProvider = true
apiMethod = "addSentShare"
username = getUsername(ctx)
token = randSeq(10)
} else {
apiMethod = "addReceivedShare"
username = g.Grantee.GetUserId().OpaqueId
}
var userID *userpb.UserId
if !isOwnersMeshProvider {
// Since this call is on the remote provider, the owner of the resource is expected to be specified.
if owner == nil {
return nil, errors.New("nextcloud: owner of resource not provided")
}
userID = owner
} else {
userID = ctxpkg.ContextMustGetUser(ctx).GetId()
}
// do not allow share to myself if share is for a user
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(g.Grantee.GetUserId(), userID) {
return nil, errors.New("nextcloud: user and grantee are the same")
}
s := &ocm.Share{
Name: name,
ResourceId: md,
Permissions: g.Permissions,
Grantee: g.Grantee,
Owner: userID,
Creator: userID,
ShareType: st,
}
var encShare []byte
var err error
if isOwnersMeshProvider {
// adding the webdav sharedSecret in the Grantee because Share itself doesn't have an Opaque field,
// see https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.Grantee
// and https://cs3org.github.io/cs3apis/#cs3.sharing.ocm.v1beta1.Share
s.Grantee.Opaque = &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
"sharedSecret": {
Decoder: "plain",
Value: []byte(token),
},
},
}
encShare, err = utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, err
}
} else {
// adding the webdav sharedSecret and remote share id (called the "ProviderID" in OCM) in the Grantee because Share itself doesn't have an Opaque field,
// see https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.Grantee
// and https://cs3org.github.io/cs3apis/#cs3.sharing.ocm.v1beta1.Share
s.Grantee.Opaque = &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
"sharedSecret": {
Decoder: "plain",
Value: []byte(token),
},
"remoteShareId": {
Decoder: "plain",
Value: g.Grantee.Opaque.Map["remoteShareId"].Value,
},
},
}
encShare, err = utils.MarshalProtoV1ToJSON(&ocm.ReceivedShare{
Share: s,
State: ocm.ShareState_SHARE_STATE_PENDING,
})
if err != nil {
return nil, err
}
}
_, body, err := sm.do(ctx, Action{apiMethod, string(encShare)}, username)
s.Id = &ocm.ShareId{
OpaqueId: string(body),
}
now := time.Now().UnixNano()
s.Ctime = &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
if err != nil {
return nil, err
}
if isOwnersMeshProvider {
// token, ok := ctxpkg.ContextGetToken(ctx)
// if !ok {
// return nil, errors.New("Could not get token from context")
// }
var protocol map[string]interface{}
if st == ocm.Share_SHARE_TYPE_TRANSFER {
protocol = map[string]interface{}{
"name": "datatx",
"options": map[string]string{
"permissions": pm,
"token": token, // FIXME: Where is the token for datatx generated?
},
}
} else {
protocol = map[string]interface{}{
"name": "webdav",
"options": map[string]string{
"permissions": pm,
"sharedSecret": token,
},
}
}
requestBodyMap := map[string]interface{}{
"shareWith": g.Grantee.GetUserId().OpaqueId,
"name": name,
"providerId": s.Id.OpaqueId,
"owner": userID.OpaqueId,
"protocol": protocol,
"meshProvider": userID.Idp, // FIXME: move this into the 'owner' string?
}
err = sender.Send(requestBodyMap, pi)
if err != nil {
err = errors.Wrap(err, "error sending OCM POST")
return nil, err
}
}
return s, nil
}
// GetShare as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm *Manager) GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) {
bodyStr, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"GetShare", string(bodyStr)}, getUsername(ctx))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
err = json.Unmarshal(body, &altResult)
if altResult == nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
ResourceId: altResult.ResourceID,
Permissions: altResult.Permissions,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, err
}
// Unshare as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm *Manager) Unshare(ctx context.Context, ref *ocm.ShareReference) error {
bodyStr, err := json.Marshal(ref)
if err != nil {
return err
}
_, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}, getUsername(ctx))
return err
}
// UpdateShare as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm *Manager) UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) {
type paramsObj struct {
Ref *ocm.ShareReference `json:"ref"`
P *ocm.SharePermissions `json:"p"`
}
bodyObj := &paramsObj{
Ref: ref,
P: p,
}
bodyStr, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"UpdateShare", string(bodyStr)}, getUsername(ctx))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
err = json.Unmarshal(body, &altResult)
if altResult == nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
ResourceId: altResult.ResourceID,
Permissions: altResult.Permissions,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, err
}
// ListShares as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm *Manager) ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
bodyStr, err := json.Marshal(filters)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"ListShares", string(bodyStr)}, getUsername(ctx))
if err != nil {
return nil, err
}
var respArr []ShareAltMap
err = json.Unmarshal(respBody, &respArr)
if err != nil {
return nil, err
}
var pointers = make([]*ocm.Share, len(respArr))
for i := 0; i < len(respArr); i++ {
altResult := respArr[i]
pointers[i] = &ocm.Share{
Id: altResult.ID,
ResourceId: altResult.ResourceID,
Permissions: altResult.Permissions,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}
}
return pointers, err
}
// ListReceivedShares as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm *Manager) ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) {
_, respBody, err := sm.do(ctx, Action{"ListReceivedShares", string("")}, getUsername(ctx))
if err != nil {
return nil, err
}
var respArr []ReceivedShareAltMap
err = json.Unmarshal(respBody, &respArr)
if err != nil {
return nil, err
}
var pointers = make([]*ocm.ReceivedShare, len(respArr))
for i := 0; i < len(respArr); i++ {
altResultShare := respArr[i].Share
if altResultShare == nil {
pointers[i] = &ocm.ReceivedShare{
Share: nil,
State: respArr[i].State,
}
} else {
pointers[i] = &ocm.ReceivedShare{
Share: &ocm.Share{
Id: altResultShare.ID,
ResourceId: altResultShare.ResourceID,
Permissions: altResultShare.Permissions,
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
},
State: respArr[i].State,
}
}
}
return pointers, err
}
// GetReceivedShare as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L29-L54
func (sm *Manager) GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
bodyStr, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"GetReceivedShare", string(bodyStr)}, getUsername(ctx))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
err = json.Unmarshal(respBody, &altResult)
if err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
Share: nil,
State: altResult.State,
}, err
}
return &ocm.ReceivedShare{
Share: &ocm.Share{
Id: altResultShare.ID,
ResourceId: altResultShare.ResourceID,
Permissions: altResultShare.Permissions,
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
},
State: altResult.State,
}, err
}
// UpdateReceivedShare as defined in the ocm.share.Manager interface
// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
func (sm Manager) UpdateReceivedShare(ctx context.Context, receivedShare *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
type paramsObj struct {
ReceivedShare *ocm.ReceivedShare `json:"received_share"`
FieldMask *field_mask.FieldMask `json:"field_mask"`
}
bodyObj := &paramsObj{
ReceivedShare: receivedShare,
FieldMask: fieldMask,
}
bodyStr, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"UpdateReceivedShare", string(bodyStr)}, getUsername(ctx))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
err = json.Unmarshal(respBody, &altResult)
if err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
Share: nil,
State: altResult.State,
}, err
}
return &ocm.ReceivedShare{
Share: &ocm.Share{
Id: altResultShare.ID,
ResourceId: altResultShare.ResourceID,
Permissions: altResultShare.Permissions,
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
},
State: altResult.State,
}, err
}
+517
View File
@@ -0,0 +1,517 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package json
import (
"context"
"encoding/json"
"io"
"os"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/google/uuid"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
)
func init() {
registry.Register("json", New)
}
// New returns a new authorizer object.
func New(m map[string]interface{}) (share.Repository, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
// load or create file
model, err := loadOrCreate(c.File)
if err != nil {
err = errors.Wrap(err, "error loading the file containing the shares")
return nil, err
}
mgr := &mgr{
c: &c,
model: model,
}
return mgr, nil
}
func loadOrCreate(file string) (*shareModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the file: "+file)
}
}
f, err := os.OpenFile(file, os.O_RDONLY, 0644)
if err != nil {
err = errors.Wrap(err, "error opening the file: "+file)
return nil, err
}
defer f.Close()
var m shareModel
if err := json.NewDecoder(f).Decode(&m); err != nil {
if err != io.EOF {
return nil, errors.Wrap(err, "error decoding data to json")
}
}
if m.Shares == nil {
m.Shares = map[string]*ocm.Share{}
}
if m.ReceivedShares == nil {
m.ReceivedShares = map[string]*ocm.ReceivedShare{}
}
return &m, nil
}
type shareModel struct {
Shares map[string]*ocm.Share `json:"shares"` // share_id -> share
ReceivedShares map[string]*ocm.ReceivedShare `json:"received_shares"` // share_id -> share
}
func (s *shareModel) UnmarshalJSON(d []byte) error {
m := struct {
Shares map[string]json.RawMessage `json:"shares"`
ReceivedShares map[string]json.RawMessage `json:"received_shares"`
}{}
if err := json.Unmarshal(d, &m); err != nil {
return err
}
share := map[string]*ocm.Share{}
for k, v := range m.Shares {
var s ocm.Share
if err := utils.UnmarshalJSONToProtoV1(v, &s); err != nil {
return err
}
share[k] = &s
}
received := map[string]*ocm.ReceivedShare{}
for k, v := range m.ReceivedShares {
var s ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1(v, &s); err != nil {
return err
}
received[k] = &s
}
*s = shareModel{
Shares: share,
ReceivedShares: received,
}
return nil
}
func (s *shareModel) MarshalJSON() ([]byte, error) {
shares := map[string]json.RawMessage{}
for k, v := range s.Shares {
d, err := utils.MarshalProtoV1ToJSON(v)
if err != nil {
return nil, err
}
shares[k] = d
}
received := map[string]json.RawMessage{}
for k, v := range s.ReceivedShares {
d, err := utils.MarshalProtoV1ToJSON(v)
if err != nil {
return nil, err
}
received[k] = d
}
return json.Marshal(map[string]any{
"shares": shares,
"received_shares": received,
})
}
type config struct {
File string `mapstructure:"file"`
}
func (c *config) ApplyDefaults() {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-shares.json"
}
}
type mgr struct {
c *config
sync.Mutex // concurrent access to the file
model *shareModel
}
func (m *mgr) save() error {
f, err := os.OpenFile(m.c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.Wrap(err, "error opening file "+m.c.File)
}
defer f.Close()
if err := json.NewEncoder(f).Encode(m.model); err != nil {
return errors.Wrap(err, "error encoding to json")
}
return f.Sync()
}
func (m *mgr) load() error {
f, err := os.OpenFile(m.c.File, os.O_RDONLY, 0644)
if err != nil {
return errors.Wrap(err, "error opening file "+m.c.File)
}
defer f.Close()
d, err := io.ReadAll(f)
if err != nil {
return err
}
var model shareModel
if err := json.Unmarshal(d, &model); err != nil {
return err
}
m.model = &model
return nil
}
func genID() string {
return uuid.New().String()
}
func (m *mgr) StoreShare(ctx context.Context, ocmshare *ocm.Share) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
if _, err := m.getByKey(ctx, &ocm.ShareKey{
Owner: ocmshare.Owner,
ResourceId: ocmshare.ResourceId,
Grantee: ocmshare.Grantee,
}); err == nil {
return nil, share.ErrShareAlreadyExisting
}
ocmshare.Id = &ocm.ShareId{OpaqueId: genID()}
clone, err := cloneShare(ocmshare)
if err != nil {
return nil, err
}
m.model.Shares[ocmshare.Id.OpaqueId] = clone
if err := m.save(); err != nil {
return nil, errors.Wrap(err, "error saving share")
}
return ocmshare, nil
}
func cloneShare(s *ocm.Share) (*ocm.Share, error) {
d, err := utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, errtypes.InternalError("failed to marshal ocm share")
}
var cloned ocm.Share
if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil {
return nil, errtypes.InternalError("failed to unmarshal ocm share")
}
return &cloned, nil
}
func cloneReceivedShare(s *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
d, err := utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, errtypes.InternalError("failed to marshal ocm received share")
}
var cloned ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil {
return nil, errtypes.InternalError("failed to unmarshal ocm received share")
}
return &cloned, nil
}
func (m *mgr) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
var (
s *ocm.Share
err error
)
if err := m.load(); err != nil {
return nil, err
}
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getByKey(ctx, ref.GetKey())
case ref.GetToken() != "":
return m.getByToken(ctx, ref.GetToken())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
// check if we are the owner
if utils.UserEqual(user.Id, s.Owner) || utils.UserEqual(user.Id, s.Creator) {
return s, nil
}
return nil, share.ErrShareNotFound
}
func (m *mgr) getByToken(ctx context.Context, token string) (*ocm.Share, error) {
for _, share := range m.model.Shares {
if share.Token == token {
return share, nil
}
}
return nil, errtypes.NotFound(token)
}
func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) {
if share, ok := m.model.Shares[id.OpaqueId]; ok {
return share, nil
}
return nil, errtypes.NotFound(id.String())
}
func (m *mgr) getByKey(ctx context.Context, key *ocm.ShareKey) (*ocm.Share, error) {
for _, share := range m.model.Shares {
if (utils.UserEqual(key.Owner, share.Owner) || utils.UserEqual(key.Owner, share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, share.ResourceId) && utils.GranteeEqual(key.Grantee, share.Grantee) {
return share, nil
}
}
return nil, share.ErrShareNotFound
}
func (m *mgr) DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return err
}
for id, share := range m.model.Shares {
if sharesEqual(ref, share) {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
delete(m.model.Shares, id)
return m.save()
}
}
}
return errtypes.NotFound(ref.String())
}
func sharesEqual(ref *ocm.ShareReference, s *ocm.Share) bool {
if ref.GetId() != nil && s.Id != nil {
if ref.GetId().OpaqueId == s.Id.OpaqueId {
return true
}
} else if ref.GetKey() != nil {
if (utils.UserEqual(ref.GetKey().Owner, s.Owner) || utils.UserEqual(ref.GetKey().Owner, s.Creator)) &&
utils.ResourceIDEqual(ref.GetKey().ResourceId, s.ResourceId) && utils.GranteeEqual(ref.GetKey().Grantee, s.Grantee) {
return true
}
}
return false
}
func receivedShareEqual(ref *ocm.ShareReference, s *ocm.ReceivedShare) bool {
if ref.GetId() != nil && s.Id != nil {
if ref.GetId().OpaqueId == s.Id.OpaqueId {
return true
}
}
return false
}
func (m *mgr) UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error) {
return nil, errtypes.NotSupported("not yet implemented")
}
func (m *mgr) ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
var ss []*ocm.Share
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.Shares {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
// no filter we return earlier
if len(filters) == 0 {
ss = append(ss, share)
} else {
// check filters
// TODO(labkode): add the rest of filters.
for _, f := range filters {
if f.Type == ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID {
if utils.ResourceIDEqual(share.ResourceId, f.GetResourceId()) {
ss = append(ss, share)
}
}
}
}
}
}
return ss, nil
}
func (m *mgr) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
share.Id = &ocm.ShareId{
OpaqueId: genID(),
}
share.Ctime = ts
share.Mtime = ts
clone, err := cloneReceivedShare(share)
if err != nil {
return nil, err
}
m.model.ReceivedShares[share.Id.OpaqueId] = clone
if err := m.save(); err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error) {
var rss []*ocm.ReceivedShare
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.ReceivedShares {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
// omit shares created by me
continue
}
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
rss = append(rss, share)
}
}
return rss, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.ReceivedShares {
if receivedShareEqual(ref, share) {
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
return share, nil
}
}
}
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
rs, err := m.GetReceivedShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Id}})
if err != nil {
return nil, err
}
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, mask := range fieldMask.Paths {
switch mask {
case "state":
rs.State = share.State
m.model.ReceivedShares[share.Id.OpaqueId].State = share.State
// TODO case "mount_point":
default:
return nil, errtypes.NotSupported("updating " + mask + " is not supported")
}
}
if err := m.save(); err != nil {
return nil, errors.Wrap(err, "error saving model")
}
return rs, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@
package loader
import (
// Load core share manager drivers.
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/manager/json"
_ "github.com/cs3org/reva/v2/pkg/ocm/invite/manager/memory"
// Add your own here
// Load core share repository drivers.
_ "github.com/cs3org/reva/v2/pkg/ocm/share/repository/json"
_ "github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud"
// Add your own here.
)
@@ -0,0 +1,437 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
package nextcloud
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
)
func init() {
registry.Register("nextcloud", New)
}
// Manager is the Nextcloud-based implementation of the share.Repository interface
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
type Manager struct {
client *http.Client
sharedSecret string
webDAVHost string
endPoint string
}
// ShareManagerConfig contains config for a Nextcloud-based ShareManager.
type ShareManagerConfig struct {
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
SharedSecret string `mapstructure:"shared_secret"`
WebDAVHost string `mapstructure:"webdav_host"`
MockHTTP bool `mapstructure:"mock_http"`
}
// Action describes a REST request to forward to the Nextcloud backend.
type Action struct {
verb string
argS string
}
// GranteeAltMap is an alternative map to JSON-unmarshal a Grantee
// Grantees are hard to unmarshal, so unmarshalling into a map[string]interface{} first,
// see also https://github.com/pondersource/sciencemesh-nextcloud/issues/27
type GranteeAltMap struct {
ID *provider.Grantee_UserId `json:"id"`
}
// ShareAltMap is an alternative map to JSON-unmarshal a Share.
type ShareAltMap struct {
ID *ocm.ShareId `json:"id"`
RemoteShareID string `json:"remote_share_id"`
Permissions *ocm.SharePermissions `json:"permissions"`
Grantee *GranteeAltMap `json:"grantee"`
Owner *userpb.UserId `json:"owner"`
Creator *userpb.UserId `json:"creator"`
Ctime *typespb.Timestamp `json:"ctime"`
Mtime *typespb.Timestamp `json:"mtime"`
}
// ReceivedShareAltMap is an alternative map to JSON-unmarshal a ReceivedShare.
type ReceivedShareAltMap struct {
Share *ShareAltMap `json:"share"`
State ocm.ShareState `json:"state"`
}
// New returns a share manager implementation that verifies against a Nextcloud backend.
func New(m map[string]interface{}) (share.Repository, error) {
var c ShareManagerConfig
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
return NewShareManager(&c)
}
// NewShareManager returns a new Nextcloud-based ShareManager.
func NewShareManager(c *ShareManagerConfig) (*Manager, error) {
var client *http.Client
if c.MockHTTP {
// called := make([]string, 0)
// nextcloudServerMock := GetNextcloudServerMock(&called)
// client, _ = TestingHTTPClient(nextcloudServerMock)
// Wait for SetHTTPClient to be called later
client = nil
} else {
if len(c.EndPoint) == 0 {
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.ocmshareprovider.drivers.nextcloud]' and '[grpc.services.ocmcore.drivers.nextcloud]'")
}
client = &http.Client{}
}
return &Manager{
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
sharedSecret: c.SharedSecret,
client: client,
webDAVHost: c.WebDAVHost,
}, nil
}
// SetHTTPClient sets the HTTP client.
func (sm *Manager) SetHTTPClient(c *http.Client) {
sm.client = c
}
// StoreShare stores a share.
func (sm *Manager) StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) {
encShare, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"addSentShare", string(encShare)}, getUsername(&userpb.User{Id: share.Creator}))
if err != nil {
return nil, err
}
share.Id = &ocm.ShareId{
OpaqueId: string(body),
}
return share, nil
}
// GetShare gets the information for a share by the given ref.
func (sm *Manager) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error) {
data, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"GetShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
if err := json.Unmarshal(body, &altResult); err != nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, nil
}
// DeleteShare deletes the share pointed by ref.
func (sm *Manager) DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
bodyStr, err := json.Marshal(ref)
if err != nil {
return err
}
_, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}, getUsername(user))
return err
}
// UpdateShare updates the mode of the given share.
func (sm *Manager) UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error) {
type paramsObj struct {
Ref *ocm.ShareReference `json:"ref"`
P *ocm.SharePermissions `json:"p"`
}
bodyObj := &paramsObj{
Ref: ref,
}
data, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"UpdateShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
if err := json.Unmarshal(body, &altResult); err != nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, nil
}
// ListShares returns the shares created by the user. If md is provided is not nil,
// it returns only shares attached to the given resource.
func (sm *Manager) ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
data, err := json.Marshal(filters)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"ListShares", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
var respArr []ShareAltMap
if err := json.Unmarshal(respBody, &respArr); err != nil {
return nil, err
}
var lst = make([]*ocm.Share, 0, len(respArr))
for _, altResult := range respArr {
lst = append(lst, &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
})
}
return lst, nil
}
// StoreReceivedShare stores a received share.
func (sm *Manager) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
data, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"addReceivedShare", string(data)}, getUsername(&userpb.User{Id: share.Grantee.GetUserId()}))
if err != nil {
return nil, err
}
share.Id = &ocm.ShareId{
OpaqueId: string(body),
}
return share, nil
}
// ListReceivedShares returns the list of shares the user has access.
func (sm *Manager) ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error) {
log := appctx.GetLogger(ctx)
_, respBody, err := sm.do(ctx, Action{"ListReceivedShares", ""}, getUsername(user))
if err != nil {
return nil, err
}
var respArr []ReceivedShareAltMap
if err := json.Unmarshal(respBody, &respArr); err != nil {
return nil, err
}
res := make([]*ocm.ReceivedShare, 0, len(respArr))
for _, share := range respArr {
altResultShare := share.Share
log.Info().Msgf("Unpacking share object %+v\n", altResultShare)
if altResultShare == nil {
continue
}
res = append(res, &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: share.State,
})
}
return res, nil
}
// GetReceivedShare returns the information for a received share the user has access.
func (sm *Manager) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
data, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"GetReceivedShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
if err := json.Unmarshal(respBody, &altResult); err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
State: altResult.State,
}, nil
}
return &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: altResult.State,
}, nil
}
// UpdateReceivedShare updates the received share with share state.
func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
type paramsObj struct {
ReceivedShare *ocm.ReceivedShare `json:"received_share"`
FieldMask *field_mask.FieldMask `json:"field_mask"`
}
bodyObj := &paramsObj{
ReceivedShare: share,
FieldMask: fieldMask,
}
bodyStr, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"UpdateReceivedShare", string(bodyStr)}, getUsername(user))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
err = json.Unmarshal(respBody, &altResult)
if err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
State: altResult.State,
}, nil
}
return &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: altResult.State,
}, nil
}
func getUsername(user *userpb.User) string {
if user != nil && len(user.Username) > 0 {
return user.Username
}
if user != nil && len(user.Id.OpaqueId) > 0 {
return user.Id.OpaqueId
}
return "empty-username"
}
func (sm *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
url := sm.endPoint + "~" + username + "/api/ocm/" + a.verb
log := appctx.GetLogger(ctx)
log.Info().Msgf("am.do %s %s", url, a.argS)
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
if err != nil {
return 0, nil, err
}
req.Header.Set("X-Reva-Secret", sm.sharedSecret)
req.Header.Set("Content-Type", "application/json")
resp, err := sm.client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, err
}
// curl -i -H 'application/json' -H 'X-Reva-Secret: shared-secret-1' -d '{"md":{"opaque_id":"fileid-/other/q/as"},"g":{"grantee":{"type":1,"Id":{"UserId":{"idp":"revanc2.docker","opaque_id":"marie"}}},"permissions":{"permissions":{"get_path":true,"initiate_file_download":true,"list_container":true,"list_file_versions":true,"stat":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' https://nc1.docker/index.php/apps/sciencemesh/~/api/ocm/addSentShare
log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return 0, nil, fmt.Errorf("Unexpected response code from EFSS API: " + strconv.Itoa(resp.StatusCode))
}
return resp.StatusCode, body, nil
}
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import (
)
// Response contains data for the Nextcloud mock server to respond
// and to switch to a new server state
// and to switch to a new server state.
type Response struct {
code int
body string
@@ -51,7 +51,7 @@ var responses = map[string]Response{
`POST /apps/sciencemesh/~tester/api/ocm/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}]`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/ListReceivedShares `: {200, `[{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}]`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/UpdateReceivedShare {"received_share":{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/UpdateReceivedShare {"received_share":{"id":{},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/addReceivedShare {"md":{"opaque_id":"fileid-/some/path"},"g":{"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"permissions":{"permissions":{"get_path":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
@@ -60,10 +60,10 @@ var responses = map[string]Response{
`POST /index.php/apps/sciencemesh/~marie/api/ocm/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}]`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/ListReceivedShares `: {200, `[{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}]`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/UpdateReceivedShare {"received_share":{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/UpdateReceivedShare {"received_share":{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
}
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server.
func GetNextcloudServerMock(called *[]string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := new(strings.Builder)
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -18,16 +18,18 @@
package registry
import "github.com/cs3org/reva/v2/pkg/ocm/invite"
import (
"github.com/cs3org/reva/v2/pkg/ocm/share"
)
// NewFunc is the function that invite managers
// NewFunc is the function that share repositories
// should register at init time.
type NewFunc func(map[string]interface{}) (invite.Manager, error)
type NewFunc func(map[string]interface{}) (share.Repository, error)
// NewFuncs is a map containing all the registered invite managers.
// NewFuncs is a map containing all the registered share repositories.
var NewFuncs = map[string]NewFunc{}
// Register registers a new invite manager new function.
// Register registers a new share repository new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
-93
View File
@@ -1,93 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package sender
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/pkg/errors"
)
const createOCMCoreShareEndpoint = "shares"
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("json: ocm endpoint not specified for mesh provider")
}
// Send executes the POST to the OCM shares endpoint to create the share at the
// remote site.
func Send(requestBodyMap map[string]interface{}, pi *ocmprovider.ProviderInfo) error {
requestBody, err := json.Marshal(requestBodyMap)
if err != nil {
err = errors.Wrap(err, "error marshalling request body")
return err
}
ocmEndpoint, err := getOCMEndpoint(pi)
if err != nil {
return err
}
u, err := url.Parse(ocmEndpoint)
if err != nil {
return err
}
u.Path = path.Join(u.Path, createOCMCoreShareEndpoint)
recipientURL := u.String()
req, err := http.NewRequest("POST", recipientURL, strings.NewReader(string(requestBody)))
if err != nil {
return errors.Wrap(err, "sender: error framing post request")
}
req.Header.Set("Content-Type", "application/json; param=value")
client := rhttp.GetHTTPClient(
rhttp.Timeout(5 * time.Second),
)
resp, err := client.Do(req)
if err != nil {
err = errors.Wrap(err, "sender: error sending post request")
return err
}
defer resp.Body.Close()
if (resp.StatusCode != http.StatusCreated) && (resp.StatusCode != http.StatusOK) {
respBody, e := io.ReadAll(resp.Body)
if e != nil {
e = errors.Wrap(e, "sender: error reading request body")
return e
}
err = errors.Wrap(fmt.Errorf("%s: %s", resp.Status, string(respBody)), "sender: error sending create ocm core share post request")
return err
}
return nil
}
+24 -15
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2021 CERN
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -22,39 +22,41 @@ import (
"context"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"google.golang.org/genproto/protobuf/field_mask"
)
// Manager is the interface that manipulates the OCM shares.
type Manager interface {
// Create a new share in fn with the given acl.
Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string,
pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error)
// Repository is the interface that manipulates the OCM shares repository.
type Repository interface {
// StoreShare stores a share.
StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error)
// GetShare gets the information for a share by the given ref.
GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error)
GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error)
// Unshare deletes the share pointed by ref.
Unshare(ctx context.Context, ref *ocm.ShareReference) error
// DeleteShare deletes the share pointed by ref.
DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error
// UpdateShare updates the mode of the given share.
UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error)
UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error)
// ListShares returns the shares created by the user. If md is provided is not nil,
// it returns only shares attached to the given resource.
ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error)
ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error)
// StoreReceivedShare stores a received share.
StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error)
// ListReceivedShares returns the list of shares the user has access.
ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error)
ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error)
// GetReceivedShare returns the information for a received share the user has access.
GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error)
GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error)
// UpdateReceivedShare updates the received share with share state.
UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error)
UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error)
}
// ResourceIDFilter is an abstraction for creating filter by resource id.
@@ -66,3 +68,10 @@ func ResourceIDFilter(id *provider.ResourceId) *ocm.ListOCMSharesRequest_Filter
},
}
}
// ErrShareAlreadyExisting is the error returned when the share already exists
// for the 3-tuple consisting of (owner, resource, grantee).
var ErrShareAlreadyExisting = errtypes.AlreadyExists("share already exists")
// ErrShareNotFound is the error returned where the share does not exist.
var ErrShareNotFound = errtypes.NotFound("share not found")
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package share
import (
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// NewWebDAVProtocol is an abstraction for creating a WebDAV protocol.
func NewWebDAVProtocol(uri, shareSecred string, perms *ocm.SharePermissions) *ocm.Protocol {
return &ocm.Protocol{
Term: &ocm.Protocol_WebdavOptions{
WebdavOptions: &ocm.WebDAVProtocol{
Uri: uri,
SharedSecret: shareSecred,
Permissions: perms,
},
},
}
}
// NewWebappProtocol is an abstraction for creating a Webapp protocol.
func NewWebappProtocol(uriTemplate string, viewMode appprovider.ViewMode) *ocm.Protocol {
return &ocm.Protocol{
Term: &ocm.Protocol_WebappOptions{
WebappOptions: &ocm.WebappProtocol{
UriTemplate: uriTemplate,
ViewMode: viewMode,
},
},
}
}
// NewTransferProtocol is an abstraction for creating a Transfer protocol.
func NewTransferProtocol(sourceURI, sharedSecret string, size uint64) *ocm.Protocol {
return &ocm.Protocol{
Term: &ocm.Protocol_TransferOptions{
TransferOptions: &ocm.TransferProtocol{
SourceUri: sourceURI,
SharedSecret: sharedSecret,
Size: size,
},
},
}
}
// NewWebDavAccessMethod is an abstraction for creating a WebDAV access method.
func NewWebDavAccessMethod(perms *provider.ResourcePermissions) *ocm.AccessMethod {
return &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: &ocm.WebDAVAccessMethod{
Permissions: perms,
},
},
}
}
// NewWebappAccessMethod is an abstraction for creating a Webapp access method.
func NewWebappAccessMethod(mode appprovider.ViewMode) *ocm.AccessMethod {
return &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebappOptions{
WebappOptions: &ocm.WebappAccessMethod{
ViewMode: mode,
},
},
}
}
// NewTransferAccessMethod is an abstraction for creating a Transfer access method.
func NewTransferAccessMethod() *ocm.AccessMethod {
return &ocm.AccessMethod{
Term: &ocm.AccessMethod_TransferOptions{
TransferOptions: &ocm.TransferAccessMethod{},
},
}
}
+465
View File
@@ -0,0 +1,465 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocm
import (
"context"
"io"
"io/fs"
"net/http"
"net/url"
"path/filepath"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typepb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/mime"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/storage"
"github.com/cs3org/reva/v2/pkg/storage/fs/registry"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/studio-b12/gowebdav"
)
func init() {
registry.Register("ocmreceived", New)
}
type driver struct {
c *config
gateway gateway.GatewayAPIClient
}
type config struct {
GatewaySVC string `mapstructure:"gatewaysvc"`
}
func (c *config) ApplyDefaults() {
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
// New creates an OCM storage driver.
func New(m map[string]interface{}, _ events.Stream) (storage.FS, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
gateway, err := pool.GetGatewayServiceClient(c.GatewaySVC)
if err != nil {
return nil, err
}
d := &driver{
c: &c,
gateway: gateway,
}
return d, nil
}
func shareInfoFromPath(path string) (*ocmpb.ShareId, string) {
// the path is of the type /share_id[/rel_path]
shareID, rel := router.ShiftPath(path)
return &ocmpb.ShareId{OpaqueId: shareID}, rel
}
func shareInfoFromReference(ref *provider.Reference) (*ocmpb.ShareId, string) {
if ref.ResourceId == nil {
return shareInfoFromPath(ref.Path)
}
return &ocmpb.ShareId{OpaqueId: ref.ResourceId.OpaqueId}, ref.Path
}
func (d *driver) getWebDAVFromShare(ctx context.Context, shareID *ocmpb.ShareId) (*ocmpb.ReceivedShare, string, string, error) {
// TODO: we may want to cache the share
res, err := d.gateway.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: shareID,
},
},
})
if err != nil {
return nil, "", "", err
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, "", "", errtypes.NotFound("share not found")
}
return nil, "", "", errtypes.InternalError(res.Status.Message)
}
dav, ok := getWebDAVProtocol(res.Share.Protocols)
if !ok {
return nil, "", "", errtypes.NotFound("share does not contain a WebDAV endpoint")
}
return res.Share, dav.Uri, dav.SharedSecret, nil
}
func getWebDAVProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebDAVProtocol, bool) {
for _, p := range protocols {
if dav, ok := p.Term.(*ocmpb.Protocol_WebdavOptions); ok {
return dav.WebdavOptions, true
}
}
return nil, false
}
func (d *driver) webdavClient(ctx context.Context, ref *provider.Reference) (*gowebdav.Client, *ocmpb.ReceivedShare, string, error) {
id, rel := shareInfoFromReference(ref)
share, endpoint, secret, err := d.getWebDAVFromShare(ctx, id)
if err != nil {
return nil, nil, "", err
}
endpoint, err = url.PathUnescape(endpoint)
if err != nil {
return nil, nil, "", err
}
// FIXME: it's still not clear from the OCM APIs how to use the shared secret
// will use as a token in the bearer authentication as this is the reva implementation
c := gowebdav.NewClient(endpoint, "", "")
c.SetHeader("Authorization", "Bearer "+secret)
return c, share, rel, nil
}
func (d *driver) CreateDir(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return err
}
return client.MkdirAll(rel, 0)
}
func (d *driver) Delete(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return err
}
return client.RemoveAll(rel)
}
func (d *driver) TouchFile(ctx context.Context, ref *provider.Reference, markprocessing bool, mtime string) error {
client, _, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return err
}
return client.Write(rel, []byte{}, 0)
}
func (d *driver) Move(ctx context.Context, oldRef, newRef *provider.Reference) error {
client, _, relOld, err := d.webdavClient(ctx, oldRef)
if err != nil {
return err
}
_, relNew := shareInfoFromReference(newRef)
return client.Rename(relOld, relNew, false)
}
func getPathFromShareIDAndRelPath(shareID *ocmpb.ShareId, relPath string) string {
return filepath.Join("/", shareID.OpaqueId, relPath)
}
func convertStatToResourceInfo(ref *provider.Reference, f fs.FileInfo, share *ocmpb.ReceivedShare, relPath string) *provider.ResourceInfo {
t := provider.ResourceType_RESOURCE_TYPE_FILE
if f.IsDir() {
t = provider.ResourceType_RESOURCE_TYPE_CONTAINER
}
var name string
if share.ResourceType == provider.ResourceType_RESOURCE_TYPE_FILE {
name = share.Name
} else {
name = f.Name()
}
webdav, _ := getWebDAVProtocol(share.Protocols)
return &provider.ResourceInfo{
Type: t,
Id: ref.ResourceId,
MimeType: mime.Detect(f.IsDir(), f.Name()),
Path: relPath,
Name: name,
Size: uint64(f.Size()),
Mtime: &typepb.Timestamp{
Seconds: uint64(f.ModTime().Unix()),
},
Owner: share.Creator,
PermissionSet: webdav.Permissions.Permissions,
Checksum: &provider.ResourceChecksum{
Type: provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID,
},
}
}
func (d *driver) GetMD(ctx context.Context, ref *provider.Reference, _ []string, _ []string) (*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return nil, err
}
info, err := client.Stat(rel)
if err != nil {
if gowebdav.IsErrNotFound(err) {
return nil, errtypes.NotFound(ref.GetPath())
}
return nil, err
}
return convertStatToResourceInfo(ref, info, share, rel), nil
}
func (d *driver) ListFolder(ctx context.Context, ref *provider.Reference, _ []string, _ []string) ([]*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return nil, err
}
list, err := client.ReadDir(rel)
if err != nil {
return nil, err
}
res := make([]*provider.ResourceInfo, 0, len(list))
for _, r := range list {
res = append(res, convertStatToResourceInfo(ref, r, share, utils.MakeRelativePath(filepath.Join(rel, r.Name()))))
}
return res, nil
}
func (d *driver) InitiateUpload(ctx context.Context, ref *provider.Reference, _ int64, _ map[string]string) (map[string]string, error) {
shareID, rel := shareInfoFromReference(ref)
p := getPathFromShareIDAndRelPath(shareID, rel)
return map[string]string{
"simple": p,
}, nil
}
func (d *driver) Upload(ctx context.Context, req storage.UploadRequest, _ storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
client, _, rel, err := d.webdavClient(ctx, req.Ref)
if err != nil {
return provider.ResourceInfo{}, err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = req.Length
}
})
return provider.ResourceInfo{}, client.WriteStream(rel, req.Body, 0)
}
func (d *driver) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) {
client, _, rel, err := d.webdavClient(ctx, ref)
if err != nil {
return nil, err
}
return client.ReadStream(rel)
}
func (d *driver) GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) {
shareID, rel := shareInfoFromReference(&provider.Reference{
ResourceId: id,
})
return getPathFromShareIDAndRelPath(shareID, rel), nil
}
func (d *driver) Shutdown(ctx context.Context) error {
return nil
}
func (d *driver) CreateHome(ctx context.Context) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) GetHome(ctx context.Context) (string, error) {
return "", errtypes.NotSupported("operation not supported")
}
func (d *driver) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) GetQuota(ctx context.Context, ref *provider.Reference) ( /*TotalBytes*/ uint64 /*UsedBytes*/, uint64, uint64, error) {
return 0, 0, 0, errtypes.NotSupported("operation not supported")
}
func (d *driver) CreateReference(ctx context.Context, path string, targetURI *url.URL) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) ListStorageSpaces(ctx context.Context, filters []*provider.ListStorageSpacesRequest_Filter, _ bool) ([]*provider.StorageSpace, error) {
spaceTypes := map[string]struct{}{}
var exists = struct{}{}
appendTypes := []string{}
for _, f := range filters {
if f.Type == provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE {
spaceType := f.GetSpaceType()
if spaceType == "+mountpoint" {
appendTypes = append(appendTypes, strings.TrimPrefix(spaceType, "+"))
continue
}
spaceTypes[spaceType] = exists
}
}
lrsRes, err := d.gateway.ListReceivedOCMShares(ctx, &ocmpb.ListReceivedOCMSharesRequest{})
if err != nil {
return nil, err
}
if len(spaceTypes) == 0 {
spaceTypes["mountpoint"] = exists
}
for _, s := range appendTypes {
spaceTypes[s] = exists
}
spaces := []*provider.StorageSpace{}
for k := range spaceTypes {
if k == "mountpoint" {
for _, share := range lrsRes.Shares {
root := &provider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: share.Id.OpaqueId,
OpaqueId: share.Id.OpaqueId,
}
space := &provider.StorageSpace{
Id: &provider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(*root),
},
SpaceType: "mountpoint",
Owner: &userv1beta1.User{
Id: share.Grantee.GetUserId(),
},
Root: root,
}
spaces = append(spaces, space)
}
}
}
return spaces, nil
}
func (d *driver) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error {
return errtypes.NotSupported("operation not supported")
}
+8
View File
@@ -131,6 +131,14 @@ func NewAlreadyExists(ctx context.Context, err error, msg string) *rpc.Status {
}
}
// NewInvalidArg returns a Status with CODE_INVALID_ARGUMENT.
func NewInvalidArg(ctx context.Context, msg string) *rpc.Status {
return &rpc.Status{Code: rpc.Code_CODE_INVALID_ARGUMENT,
Message: msg,
Trace: getTrace(ctx),
}
}
// NewConflict returns a Status with Code_CODE_ABORTED.
//
// Deprecated: NewConflict exists for historical compatibility
@@ -97,7 +97,12 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) {
defer r.Body.Close()
ref := &provider.Reference{Path: fn}
info, err := fs.Upload(ctx, ref, r.Body, func(spaceOwner, owner *userpb.UserId, ref *provider.Reference) {
info, err := fs.Upload(ctx, storage.UploadRequest{
Ref: ref,
Body: r.Body,
Length: r.ContentLength,
}, func(spaceOwner, owner *userpb.UserId, ref *provider.Reference) {
datatx.InvalidateCache(owner, ref, m.statCache)
if err := datatx.EmitFileUploadedEvent(spaceOwner, owner, ref, m.publisher); err != nil {
sublog.Error().Err(err).Msg("failed to publish FileUploaded event")
@@ -112,7 +112,11 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) {
Path: fn,
}
var info provider.ResourceInfo
info, err = fs.Upload(ctx, ref, r.Body, func(spaceOwner, owner *userpb.UserId, ref *provider.Reference) {
info, err = fs.Upload(ctx, storage.UploadRequest{
Ref: ref,
Body: r.Body,
Length: r.ContentLength,
}, func(spaceOwner, owner *userpb.UserId, ref *provider.Reference) {
datatx.InvalidateCache(owner, ref, m.statCache)
if err := datatx.EmitFileUploadedEvent(spaceOwner, owner, ref, m.publisher); err != nil {
sublog.Error().Err(err).Msg("failed to publish FileUploaded event")
+12 -3
View File
@@ -519,7 +519,7 @@ func (m *Manager) UpdateShare(ctx context.Context, ref *collaboration.ShareRefer
}
// ListReceivedShares returns the list of shares the user has access to.
func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
@@ -529,11 +529,20 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
return nil, errtypes.UserRequired("error getting user from context")
}
uid, groups := user.GetId(), user.GetGroups()
if user.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
u, err := utils.GetUser(forUser, m.gatewayClient)
if err != nil {
return nil, errtypes.BadRequest("user not found")
}
uid = forUser
groups = u.GetGroups()
}
result := []*collaboration.ReceivedShare{}
ids, err := granteeToIndex(&provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{UserId: user.Id},
Id: &provider.Grantee_UserId{UserId: uid},
})
if err != nil {
return nil, err
@@ -544,7 +553,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
if err != nil {
return nil, err
}
for _, group := range user.Groups {
for _, group := range groups {
index, err := granteeToIndex(&provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
Id: &provider.Grantee_GroupId{GroupId: &groupv1beta1.GroupId{OpaqueId: group}},
+12 -1
View File
@@ -475,11 +475,22 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) (
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
user := ctxpkg.ContextMustGetUser(ctx)
if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
gwc, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
if err != nil {
return nil, errors.Wrap(err, "failed to list shares")
}
u, err := utils.GetUser(forUser, gwc)
if err != nil {
return nil, errtypes.BadRequest("user not found")
}
user = u
}
mem := make(map[string]int)
var rss []*collaboration.ReceivedShare
for _, s := range m.model.Shares {
+8 -1
View File
@@ -759,7 +759,7 @@ func (m *Manager) listCreatedShares(ctx context.Context, user *userv1beta1.User,
}
// ListReceivedShares returns the list of shares the user has access to.
func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "ListReceivedShares")
defer span.End()
@@ -768,6 +768,13 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
}
user := ctxpkg.ContextMustGetUser(ctx)
if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
u, err := utils.GetUser(forUser, m.gateway)
if err != nil {
return nil, errtypes.BadRequest("user not found")
}
user = u
}
ssids := map[string]*receivedsharecache.Space{}
@@ -44,7 +44,7 @@ import (
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/cs3org/reva/pkg/share/manager/jsoncs3/providercache")
tracer = otel.Tracer("github.com/cs3org/reva/v2/pkg/share/manager/jsoncs3/providercache")
}
// Cache holds share information structured by provider and space
+6 -1
View File
@@ -20,6 +20,7 @@ package memory
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
@@ -282,11 +283,15 @@ func (m *manager) ListShares(ctx context.Context, filters []*collaboration.Filte
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
var rss []*collaboration.ReceivedShare
m.lock.Lock()
defer m.lock.Unlock()
user := ctxpkg.ContextMustGetUser(ctx)
if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
// TODO: gateway missing!
return nil, errors.New("can't use inmem share manager and service accounts")
}
for _, s := range m.shares {
if share.IsCreatedByUser(s, user) || !share.IsGrantedToUser(s, user) {
// omit shares created by the user or shares the user can't access
@@ -33,6 +33,7 @@ import (
conversions "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/jellydator/ttlcache/v2"
)
@@ -62,6 +63,7 @@ type DBShare struct {
type UserConverter interface {
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
GetUser(userid *userpb.UserId) (*userpb.User, error)
}
// GatewayUserConverter converts usernames and ids using the gateway
@@ -139,6 +141,15 @@ func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username st
return getUserResponse.User.Id, nil
}
// GetUser gets the user
func (c *GatewayUserConverter) GetUser(userid *userpb.UserId) (*userpb.User, error) {
gwc, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return nil, err
}
return utils.GetUser(userid, gwc)
}
func (m *mgr) formatGrantee(ctx context.Context, g *provider.Grantee) (int, string, error) {
var granteeType int
var formattedID string
@@ -336,8 +336,15 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) (
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if user.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
u, err := m.userConverter.GetUser(forUser)
if err != nil {
return nil, errtypes.BadRequest("user not found")
}
user = u
}
uid := user.Username
params := []interface{}{uid, uid, uid}
+3 -3
View File
@@ -62,13 +62,13 @@ type Manager interface {
// it returns only shares attached to the given resource.
ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error)
// ListReceivedShares returns the list of shares the user has access to.
ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error)
// ListReceivedShares returns the list of shares the user has access to. `forUser` parameter for service accounts only
ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error)
// GetReceivedShare returns the information for a received share.
GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error)
// UpdateReceivedShare updates the received share with share state.
// UpdateReceivedShare updates the received share with share state.`forUser` parameter for service accounts only
UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, forUser *userv1beta1.UserId) (*collaboration.ReceivedShare, error)
}
+8 -8
View File
@@ -42,12 +42,12 @@ import (
tusd "github.com/tus/tusd/pkg/handler"
)
func (fs *cephfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser, uff storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
func (fs *cephfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
user := fs.makeUser(ctx)
upload, err := fs.GetUpload(ctx, ref.GetPath())
upload, err := fs.GetUpload(ctx, req.Ref.GetPath())
if err != nil {
metadata := map[string]string{"sizedeferred": "true"}
uploadIDs, err := fs.InitiateUpload(ctx, ref, 0, metadata)
uploadIDs, err := fs.InitiateUpload(ctx, req.Ref, 0, metadata)
if err != nil {
return provider.ResourceInfo{}, err
}
@@ -65,7 +65,7 @@ func (fs *cephfs) Upload(ctx context.Context, ref *provider.Reference, r io.Read
}
if ok {
var assembledFile string
p, assembledFile, err = NewChunkHandler(ctx, fs).WriteChunk(p, r)
p, assembledFile, err = NewChunkHandler(ctx, fs).WriteChunk(p, req.Body)
if err != nil {
return provider.ResourceInfo{}, err
}
@@ -73,17 +73,17 @@ func (fs *cephfs) Upload(ctx context.Context, ref *provider.Reference, r io.Read
if err = uploadInfo.Terminate(ctx); err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "cephfs: error removing auxiliary files")
}
return provider.ResourceInfo{}, errtypes.PartialContent(ref.String())
return provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String())
}
uploadInfo.info.Storage["InternalDestination"] = p
user.op(func(cv *cacheVal) {
r, err = cv.mount.Open(assembledFile, os.O_RDONLY, 0)
req.Body, err = cv.mount.Open(assembledFile, os.O_RDONLY, 0)
})
if err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "cephfs: error opening assembled file")
}
defer r.Close()
defer req.Body.Close()
defer user.op(func(cv *cacheVal) {
_ = cv.mount.Unlink(assembledFile)
})
@@ -102,7 +102,7 @@ func (fs *cephfs) Upload(ctx context.Context, ref *provider.Reference, r io.Read
ri.Mtime = &mtime
}
if _, err := uploadInfo.WriteChunk(ctx, 0, r); err != nil {
if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "cephfs: error writing to binary file")
}
+1
View File
@@ -20,6 +20,7 @@ package loader
import (
// Load core storage filesystem backends.
_ "github.com/cs3org/reva/v2/pkg/ocm/storage/received"
_ "github.com/cs3org/reva/v2/pkg/storage/fs/cephfs"
_ "github.com/cs3org/reva/v2/pkg/storage/fs/eos"
_ "github.com/cs3org/reva/v2/pkg/storage/fs/eosgrpc"
+3 -3
View File
@@ -397,14 +397,14 @@ func (nc *StorageDriver) InitiateUpload(ctx context.Context, ref *provider.Refer
}
// Upload as defined in the storage.FS interface
func (nc *StorageDriver) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser, _ storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
err := nc.doUpload(ctx, ref.Path, r)
func (nc *StorageDriver) Upload(ctx context.Context, req storage.UploadRequest, _ storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
err := nc.doUpload(ctx, req.Ref.Path, req.Body)
if err != nil {
return provider.ResourceInfo{}, err
}
// return id, etag and mtime
ri, err := nc.GetMD(ctx, ref, []string{}, []string{"id", "etag", "mtime"})
ri, err := nc.GetMD(ctx, req.Ref, []string{}, []string{"id", "etag", "mtime"})
if err != nil {
return provider.ResourceInfo{}, err
}
+6 -6
View File
@@ -49,8 +49,8 @@ import (
var defaultFilePerm = os.FileMode(0664)
func (fs *owncloudsqlfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser, uff storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
upload, err := fs.GetUpload(ctx, ref.GetPath())
func (fs *owncloudsqlfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
upload, err := fs.GetUpload(ctx, req.Ref.GetPath())
if err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error retrieving upload")
}
@@ -60,7 +60,7 @@ func (fs *owncloudsqlfs) Upload(ctx context.Context, ref *provider.Reference, r
p := uploadInfo.info.Storage["InternalDestination"]
if chunking.IsChunked(p) {
var assembledFile string
p, assembledFile, err = fs.chunkHandler.WriteChunk(p, r)
p, assembledFile, err = fs.chunkHandler.WriteChunk(p, req.Body)
if err != nil {
return provider.ResourceInfo{}, err
}
@@ -68,7 +68,7 @@ func (fs *owncloudsqlfs) Upload(ctx context.Context, ref *provider.Reference, r
if err = uploadInfo.Terminate(ctx); err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error removing auxiliary files")
}
return provider.ResourceInfo{}, errtypes.PartialContent(ref.String())
return provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String())
}
uploadInfo.info.Storage["InternalDestination"] = p
fd, err := os.Open(assembledFile)
@@ -77,10 +77,10 @@ func (fs *owncloudsqlfs) Upload(ctx context.Context, ref *provider.Reference, r
}
defer fd.Close()
defer os.RemoveAll(assembledFile)
r = fd
req.Body = fd
}
if _, err := uploadInfo.WriteChunk(ctx, 0, r); err != nil {
if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil {
return provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error writing to binary file")
}

Some files were not shown because too many files have changed in this diff Show More