From f1ed2e47732f41272cda29a08bf89fe852db059d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Tue, 3 Aug 2021 11:02:44 +0200 Subject: [PATCH 01/30] Add a sharesstorageprovider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provider exposes all received shares. It can be mounted to /home/Shares to make a lot of special cases for shares handling in the gateway and storage drivers superfluous. Add missing mock file Implement methods for setting/unsetting arbitrary metadata Fix tests Adapt to changes from rebase Fix creating references with embedded mounts Fix corner cases when stating shares Allow moves between shares on the same storage Make the storage rules known to the gateway as well Do not choke on non-existent shares Reject a share when it is being deleted Fix rebase artifacts WIP: Refactor statting shares. Merge shares permissions. update after rebase, fix tests Signed-off-by: Jörn Friedrich Dreyer list all shares Signed-off-by: Jörn Friedrich Dreyer work on api change Signed-off-by: Jörn Friedrich Dreyer Fix build Persist mountpoints in the share managers followin the new cs3 api Add support for renaming shares in the SharesStorageprovider Adapt commands for updating received shares Regenerate the share manager mock Fix linter warning Do not raise an internal error when trying to access non-existent shares Make hound happy Fix wrong column name in query Fix typo Do not confuse user and group names Add test for listing received group shares Do not list parent group shares if there is a child share for it already Make hound happy Hide the fact that accepted groups shares can be child shares in the db list shares using the shares manager + hide group shares when the same resource has a user and group share refactor all the ocs error writing from the new code Only collide with mountpoints of shares pointing do different resources Also return shares being shared with one of the user's groups Add sharesstorageprovider service file for local acceptance tests Adapt nextcloud share manager to new method signature Also remove the test for UpdateReceivedShare which can not be tested anymore with the new signature. The ReceivedShare never held the display name that's being tested so the test only passed on the data from the update field, but since the method only takes the actual received share now this is no longer possible. WIP: use go-cs3apis fork until it has been merged Add placeholder changelog to make CI run Tweak documentation on how to run the acceptance tests Add missing storage registry rule for the sharesstorageprovider Fix revad config for local acceptance tests Signed-off-by: Jörn Friedrich Dreyer --- .drone.star | 1 + internal/grpc/services/gateway/appprovider.go | 10 - internal/grpc/services/gateway/gateway.go | 43 +- .../services/gateway/publicshareprovider.go | 5 - .../grpc/services/gateway/storageprovider.go | 1262 ++--------------- .../services/gateway/usershareprovider.go | 72 +- .../services/gateway/webdavstorageprovider.go | 215 --- internal/grpc/services/loader/loader.go | 1 + .../mocks/GatewayClient.go | 367 +++++ .../mocks/SharesProviderClient.go | 96 ++ .../sharesstorageprovider.go | 980 +++++++++++++ .../sharesstorageprovider_suite_test.go | 31 + .../sharesstorageprovider_test.go | 762 ++++++++++ .../usershareprovider/usershareprovider.go | 24 +- internal/http/services/owncloud/ocdav/dav.go | 3 + .../http/services/owncloud/ocdav/ocdav.go | 1 + .../handlers/apps/sharing/shares/pending.go | 152 +- .../owncloud/ocs/response/response.go | 11 + pkg/cbox/share/sql/sql.go | 4 +- pkg/ocm/share/manager/json/json.go | 3 +- pkg/share/manager/json/json.go | 70 +- pkg/share/manager/memory/memory.go | 44 +- pkg/share/manager/sql/conversions.go | 13 +- pkg/share/manager/sql/sql.go | 167 ++- pkg/share/manager/sql/sql_test.go | 237 +++- .../utils/decomposedfs/decomposedfs.go | 39 +- tests/oc-integration-tests/drone/gateway.toml | 8 + .../drone/storage-shares-ocis.toml | 14 + .../oc-integration-tests/local/frontend.toml | 2 +- tests/oc-integration-tests/local/gateway.toml | 8 + .../local/storage-shares.toml | 14 + .../local/storage-users.toml | 2 +- 32 files changed, 3100 insertions(+), 1561 deletions(-) create mode 100644 internal/grpc/services/sharesstorageprovider/mocks/GatewayClient.go create mode 100644 internal/grpc/services/sharesstorageprovider/mocks/SharesProviderClient.go create mode 100644 internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go create mode 100644 internal/grpc/services/sharesstorageprovider/sharesstorageprovider_suite_test.go create mode 100644 internal/grpc/services/sharesstorageprovider/sharesstorageprovider_test.go create mode 100644 tests/oc-integration-tests/drone/storage-shares-ocis.toml create mode 100644 tests/oc-integration-tests/local/storage-shares.toml diff --git a/.drone.star b/.drone.star index 494c282bb0..9a9799f41e 100644 --- a/.drone.star +++ b/.drone.star @@ -687,6 +687,7 @@ def ocisIntegrationTests(parallelRuns, skipExceptParts = []): "/drone/src/cmd/revad/revad -c storage-home-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-oc-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-publiclink-ocis.toml &", + "/drone/src/cmd/revad/revad -c storage-shares-ocis.toml &", "/drone/src/cmd/revad/revad -c ldap-users.toml", ], }, diff --git a/internal/grpc/services/gateway/appprovider.go b/internal/grpc/services/gateway/appprovider.go index 5ad8f082f5..5e288429a7 100644 --- a/internal/grpc/services/gateway/appprovider.go +++ b/internal/grpc/services/gateway/appprovider.go @@ -56,17 +56,7 @@ func (s *svc) OpenInApp(ctx context.Context, req *gateway.OpenInAppRequest) (*pr }, nil } - if s.isSharedFolder(ctx, p) { - return &providerpb.OpenInAppResponse{ - Status: status.NewInvalid(ctx, "gateway: can't open shares folder"), - }, nil - } - resName, resChild := p, "" - if s.isShareChild(ctx, p) { - resName, resChild = s.splitShare(ctx, p) - } - statRes, err := s.stat(ctx, &storageprovider.StatRequest{ Ref: &storageprovider.Reference{Path: resName}, }) diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index 6908174e16..00bc6ca45a 100644 --- a/internal/grpc/services/gateway/gateway.go +++ b/internal/grpc/services/gateway/gateway.go @@ -42,27 +42,28 @@ func init() { } type config struct { - AuthRegistryEndpoint string `mapstructure:"authregistrysvc"` - ApplicationAuthEndpoint string `mapstructure:"applicationauthsvc"` - StorageRegistryEndpoint string `mapstructure:"storageregistrysvc"` - AppRegistryEndpoint string `mapstructure:"appregistrysvc"` - PreferencesEndpoint string `mapstructure:"preferencessvc"` - UserShareProviderEndpoint string `mapstructure:"usershareprovidersvc"` - PublicShareProviderEndpoint string `mapstructure:"publicshareprovidersvc"` - OCMShareProviderEndpoint string `mapstructure:"ocmshareprovidersvc"` - OCMInviteManagerEndpoint string `mapstructure:"ocminvitemanagersvc"` - OCMProviderAuthorizerEndpoint string `mapstructure:"ocmproviderauthorizersvc"` - OCMCoreEndpoint string `mapstructure:"ocmcoresvc"` - UserProviderEndpoint string `mapstructure:"userprovidersvc"` - GroupProviderEndpoint string `mapstructure:"groupprovidersvc"` - DataTxEndpoint string `mapstructure:"datatx"` - DataGatewayEndpoint string `mapstructure:"datagateway"` - CommitShareToStorageGrant bool `mapstructure:"commit_share_to_storage_grant"` - CommitShareToStorageRef bool `mapstructure:"commit_share_to_storage_ref"` - DisableHomeCreationOnLogin bool `mapstructure:"disable_home_creation_on_login"` - TransferSharedSecret string `mapstructure:"transfer_shared_secret"` - TransferExpires int64 `mapstructure:"transfer_expires"` - TokenManager string `mapstructure:"token_manager"` + StorageRules map[string]map[string]interface{} `mapstructure:"storage_rules"` + AuthRegistryEndpoint string `mapstructure:"authregistrysvc"` + ApplicationAuthEndpoint string `mapstructure:"applicationauthsvc"` + StorageRegistryEndpoint string `mapstructure:"storageregistrysvc"` + AppRegistryEndpoint string `mapstructure:"appregistrysvc"` + PreferencesEndpoint string `mapstructure:"preferencessvc"` + UserShareProviderEndpoint string `mapstructure:"usershareprovidersvc"` + PublicShareProviderEndpoint string `mapstructure:"publicshareprovidersvc"` + OCMShareProviderEndpoint string `mapstructure:"ocmshareprovidersvc"` + OCMInviteManagerEndpoint string `mapstructure:"ocminvitemanagersvc"` + OCMProviderAuthorizerEndpoint string `mapstructure:"ocmproviderauthorizersvc"` + OCMCoreEndpoint string `mapstructure:"ocmcoresvc"` + UserProviderEndpoint string `mapstructure:"userprovidersvc"` + GroupProviderEndpoint string `mapstructure:"groupprovidersvc"` + DataTxEndpoint string `mapstructure:"datatx"` + DataGatewayEndpoint string `mapstructure:"datagateway"` + CommitShareToStorageGrant bool `mapstructure:"commit_share_to_storage_grant"` + CommitShareToStorageRef bool `mapstructure:"commit_share_to_storage_ref"` + DisableHomeCreationOnLogin bool `mapstructure:"disable_home_creation_on_login"` + TransferSharedSecret string `mapstructure:"transfer_shared_secret"` + TransferExpires int64 `mapstructure:"transfer_expires"` + TokenManager string `mapstructure:"token_manager"` // ShareFolder is the location where to create shares in the recipient's storage provider. ShareFolder string `mapstructure:"share_folder"` DataTransfersFolder string `mapstructure:"data_transfers_folder"` diff --git a/internal/grpc/services/gateway/publicshareprovider.go b/internal/grpc/services/gateway/publicshareprovider.go index 88bb41b1c9..2d33b5893d 100644 --- a/internal/grpc/services/gateway/publicshareprovider.go +++ b/internal/grpc/services/gateway/publicshareprovider.go @@ -24,16 +24,11 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/pkg/errors" ) func (s *svc) CreatePublicShare(ctx context.Context, req *link.CreatePublicShareRequest) (*link.CreatePublicShareResponse, error) { - if s.isSharedFolder(ctx, req.ResourceInfo.GetPath()) { - return nil, errtypes.AlreadyExists("gateway: can't create a public share of the share folder itself") - } - log := appctx.GetLogger(ctx) log.Info().Msg("create public share") diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index da46ffbd8e..2db2fd8752 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -20,7 +20,9 @@ package gateway import ( "context" + "crypto/md5" "fmt" + "io" "net/url" "path" "path/filepath" @@ -28,21 +30,15 @@ import ( "sync" "time" - "google.golang.org/protobuf/types/known/fieldmaskpb" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/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" - rtrace "github.com/cs3org/reva/pkg/trace" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/storage/utils/etag" "github.com/cs3org/reva/pkg/utils" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -309,166 +305,30 @@ func (s *svc) getHome(_ context.Context) string { } func (s *svc) InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { - log := appctx.GetLogger(ctx) - if utils.IsRelativeReference(req.Ref) { return s.initiateFileDownload(ctx, req) } - p, st := s.getPath(ctx, req.Ref) + _, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileDownloadResponse{ Status: st, }, nil } - if !s.inSharedFolder(ctx, p) { - statReq := &provider.StatRequest{Ref: req.Ref} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - if statRes.Status.Code != rpc.Code_CODE_OK { - return &gateway.InitiateFileDownloadResponse{ - Status: statRes.Status, - }, nil - } - return s.initiateFileDownload(ctx, req) - } - - if s.isSharedFolder(ctx, p) { - log.Debug().Str("path", p).Msg("path points to shared folder") - err := errtypes.PermissionDenied("gateway: cannot download share folder: path=" + p) - log.Err(err).Msg("gateway: error downloading") + statReq := &provider.StatRequest{Ref: req.Ref} + statRes, err := s.stat(ctx, statReq) + if err != nil { return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInvalidArg(ctx, "path points to share folder"), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, nil - } - - if s.isShareName(ctx, p) { - statReq := &provider.StatRequest{Ref: req.Ref} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - if statRes.Status.Code != rpc.Code_CODE_OK { - return &gateway.InitiateFileDownloadResponse{ - Status: statRes.Status, - }, nil - } - - if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { - err := errtypes.BadRequest(fmt.Sprintf("gateway: expected reference: got:%+v", statRes.Info)) - log.Err(err).Msg("gateway: error stating share name") - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error initiating download"), - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - // TODO(ishank011): pass this through the datagateway service - // For now, we just expose the file server to the user - ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), - }, nil - } - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileDownloadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - DownloadEndpoint: ep, - }, - }, - }, nil - } - - // if it is a file allow download - if ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { - log.Debug().Str("path", p).Interface("ri", ri).Msg("path points to share name file") - req.Ref.Path = ri.Path - log.Debug().Str("path", ri.Path).Msg("download") - return s.initiateFileDownload(ctx, req) - } - - log.Debug().Str("path", p).Interface("statRes", statRes).Msg("path:%s points to share name") - err = errtypes.PermissionDenied("gateway: cannot download share name: path=" + p) - log.Err(err).Str("path", p).Msg("gateway: error downloading") + if statRes.Status.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInvalidArg(ctx, "path points to share name"), + Status: statRes.Status, }, nil } - - if s.isShareChild(ctx, p) { - log.Debug().Msgf("shared child: %s", p) - shareName, shareChild := s.splitShare(ctx, p) - - statReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: shareName}, - } - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &gateway.InitiateFileDownloadResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - // TODO(ishank011): pass this through the datagateway service - // For now, we just expose the file server to the user - ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), - }, nil - } - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileDownloadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - DownloadEndpoint: ep, - }, - }, - }, nil - } - - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - log.Debug().Str("path", req.Ref.Path).Msg("download") - return s.initiateFileDownload(ctx, req) - } - - panic("gateway: download: unknown path:" + p) + return s.initiateFileDownload(ctx, req) } func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { @@ -524,150 +384,17 @@ func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFi } func (s *svc) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { - log := appctx.GetLogger(ctx) if utils.IsRelativeReference(req.Ref) { return s.initiateFileUpload(ctx, req) } - p, st := s.getPath(ctx, req.Ref) + _, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileUploadResponse{ Status: st, }, nil } - if !s.inSharedFolder(ctx, p) { - return s.initiateFileUpload(ctx, req) - } - - if s.isSharedFolder(ctx, p) { - log.Debug().Str("path", p).Msg("path points to shared folder") - err := errtypes.PermissionDenied("gateway: cannot upload to share folder: path=" + p) - log.Err(err).Msg("gateway: error downloading") - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInvalidArg(ctx, "path points to share folder"), - }, nil - - } - - if s.isShareName(ctx, p) { - log.Debug().Str("path", p).Msg("path points to share name") - statReq := &provider.StatRequest{Ref: req.Ref} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - if statRes.Status.Code != rpc.Code_CODE_OK { - return &gateway.InitiateFileUploadResponse{ - Status: statRes.Status, - }, nil - } - - if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { - err := errtypes.BadRequest(fmt.Sprintf("gateway: expected reference: got:%+v", statRes.Info)) - log.Err(err).Msg("gateway: error stating share name") - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error initiating upload"), - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - // TODO(ishank011): pass this through the datagateway service - // For now, we just expose the file server to the user - ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), - }, nil - } - return &gateway.InitiateFileUploadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileUploadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - UploadEndpoint: ep, - }, - }, - }, nil - } - - // if it is a file allow upload - if ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { - log.Debug().Str("path", p).Interface("ri", ri).Msg("path points to share name file") - req.Ref.Path = ri.Path - log.Debug().Str("path", ri.Path).Msg("upload") - return s.initiateFileUpload(ctx, req) - } - - err = errtypes.PermissionDenied("gateway: cannot upload to share name: path=" + p) - log.Err(err).Msg("gateway: error uploading") - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInvalidArg(ctx, "path points to share name"), - }, nil - - } - - if s.isShareChild(ctx, p) { - log.Debug().Msgf("shared child: %s", p) - shareName, shareChild := s.splitShare(ctx, p) - - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &gateway.InitiateFileUploadResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - // TODO(ishank011): pass this through the datagateway service - // For now, we just expose the file server to the user - ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error uploading to webdav host: "+p), - }, nil - } - return &gateway.InitiateFileUploadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileUploadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - UploadEndpoint: ep, - }, - }, - }, nil - } - - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - return s.initiateFileUpload(ctx, req) - } - - panic("gateway: upload: unknown path:" + p) + return s.initiateFileUpload(ctx, req) } func (s *svc) initiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { @@ -749,76 +476,18 @@ func (s *svc) GetPath(ctx context.Context, req *provider.GetPathRequest) (*provi } func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { - log := appctx.GetLogger(ctx) - if utils.IsRelativeReference(req.Ref) { return s.createContainer(ctx, req) } - p, st := s.getPath(ctx, req.Ref) + _, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &provider.CreateContainerResponse{ Status: st, }, nil } - if !s.inSharedFolder(ctx, p) { - return s.createContainer(ctx, req) - } - - if s.isSharedFolder(ctx, p) || s.isShareName(ctx, p) { - log.Debug().Msgf("path:%s points to shared folder or share name", p) - err := errtypes.PermissionDenied("gateway: cannot create container on share folder or share name: path=" + p) - log.Err(err).Msg("gateway: error creating container") - return &provider.CreateContainerResponse{ - Status: status.NewInvalidArg(ctx, "path points to share folder or share name"), - }, nil - - } - - if s.isShareChild(ctx, p) { - log.Debug().Msgf("shared child: %s", p) - shareName, shareChild := s.splitShare(ctx, p) - - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &provider.CreateContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.CreateContainerResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.CreateContainerResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - err = s.webdavRefMkdir(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &provider.CreateContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error creating container on webdav host: "+p), - }, nil - } - return &provider.CreateContainerResponse{ - Status: status.NewOK(ctx), - }, nil - } - - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - return s.createContainer(ctx, req) - } - - panic("gateway: create container on unknown path:" + p) + return s.createContainer(ctx, req) } func (s *svc) createContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { @@ -837,143 +506,15 @@ func (s *svc) createContainer(ctx context.Context, req *provider.CreateContainer return res, nil } -// check if the path contains the prefix of the shared folder -func (s *svc) inSharedFolder(ctx context.Context, p string) bool { - sharedFolder := s.getSharedFolder(ctx) - return strings.HasPrefix(p, sharedFolder) -} - func (s *svc) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { - log := appctx.GetLogger(ctx) - p, st := s.getPath(ctx, req.Ref) + _, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &provider.DeleteResponse{ Status: st, }, nil } - ctx, span := rtrace.Provider.Tracer("reva").Start(ctx, "Delete") - defer span.End() - - if !s.inSharedFolder(ctx, p) { - return s.delete(ctx, req) - } - - if s.isSharedFolder(ctx, p) { - // TODO(labkode): deleting share names should be allowed, means unmounting. - err := errtypes.BadRequest("gateway: cannot delete share folder or share name: path=" + p) - span.RecordError(err) - return &provider.DeleteResponse{ - Status: status.NewInvalidArg(ctx, "path points to share folder or share name"), - }, nil - - } - - if s.isShareName(ctx, p) { - log.Debug().Msgf("path:%s points to share name", p) - - sRes, err := s.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) - if err != nil { - return nil, err - } - - statRes, err := s.Stat(ctx, &provider.StatRequest{ - Ref: &provider.Reference{ - Path: p, - }, - }) - if err != nil { - return nil, err - } - - // the following will check that: - // - the resource to delete is a share the current user received - // - signal the storage the delete must not land in the trashbin - // - delete the resource and update the share status to "rejected" - for _, share := range sRes.Shares { - if statRes != nil && (share.Share.ResourceId.OpaqueId == statRes.Info.Id.OpaqueId) && (share.Share.ResourceId.StorageId == statRes.Info.Id.StorageId) { - // this opaque needs explanation. It signals the storage the resource we're about to delete does not - // belong to the current user because it was share to her, thus delete the "node" and don't send it to - // the trash bin, since the share can be mounted as many times as desired. - req.Opaque = &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "deleting_shared_resource": { - Value: []byte("true"), - Decoder: "plain", - }, - }, - } - - // the following block takes care of updating the state of the share to "rejected". This will ensure the user - // can "Accept" the share once again. - // TODO should this be pending? If so, update the two comments above as well. If not, get rid of this comment. - share.State = collaboration.ShareState_SHARE_STATE_REJECTED - r := &collaboration.UpdateReceivedShareRequest{ - Share: share, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, - } - - _, err := s.UpdateReceivedShare(ctx, r) - if err != nil { - return nil, err - } - - return &provider.DeleteResponse{ - Status: status.NewOK(ctx), - }, nil - } - } - - return &provider.DeleteResponse{ - Status: status.NewNotFound(ctx, "could not find share"), - }, nil - } - - if s.isShareChild(ctx, p) { - shareName, shareChild := s.splitShare(ctx, p) - log.Debug().Msgf("path:%s sharename:%s sharechild: %s", p, shareName, shareChild) - - ref := &provider.Reference{Path: shareName} - - statReq := &provider.StatRequest{Ref: ref} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &provider.DeleteResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.DeleteResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.DeleteResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - err = s.webdavRefDelete(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &provider.DeleteResponse{ - Status: status.NewInternal(ctx, err, "gateway: error deleting resource on webdav host: "+p), - }, nil - } - return &provider.DeleteResponse{ - Status: status.NewOK(ctx), - }, nil - } - - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - return s.delete(ctx, req) - } - - panic("gateway: delete called on unknown path:" + p) + return s.delete(ctx, req) } func (s *svc) delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { @@ -994,152 +535,55 @@ func (s *svc) delete(ctx context.Context, req *provider.DeleteRequest) (*provide } func (s *svc) Move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { - log := appctx.GetLogger(ctx) - p, st := s.getPath(ctx, req.Source) + _, st := s.getPath(ctx, req.Source) if st.Code != rpc.Code_CODE_OK { return &provider.MoveResponse{ Status: st, }, nil } - dp, st := s.getPath(ctx, req.Destination) - if st.Code != rpc.Code_CODE_OK && st.Code != rpc.Code_CODE_NOT_FOUND { + _, st2 := s.getPath(ctx, req.Destination) + if st2.Code != rpc.Code_CODE_OK && st2.Code != rpc.Code_CODE_NOT_FOUND { return &provider.MoveResponse{ - Status: st, + Status: st2, }, nil } - if !s.inSharedFolder(ctx, p) && !s.inSharedFolder(ctx, dp) { - return s.move(ctx, req) - } + return s.move(ctx, req) +} - // allow renaming the share folder, the mount point, not the target. - if s.isShareName(ctx, p) && s.isShareName(ctx, dp) { - log.Info().Msgf("gateway: move: renaming share mountpoint: from:%s to:%s", p, dp) - return s.move(ctx, req) +func (s *svc) move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { + srcProviders, err := s.findProviders(ctx, req.Source) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "move src="+req.Source.String(), err), + }, nil } - // resolve references and check the ref points to the same base path, paranoia check. - if s.isShareChild(ctx, p) && s.isShareChild(ctx, dp) { - shareName, shareChild := s.splitShare(ctx, p) - dshareName, dshareChild := s.splitShare(ctx, dp) - log.Debug().Msgf("srcpath:%s dstpath:%s srcsharename:%s srcsharechild: %s dstsharename:%s dstsharechild:%s ", p, dp, shareName, shareChild, dshareName, dshareChild) - - srcStatReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - srcStatRes, err := s.stat(ctx, srcStatReq) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+srcStatReq.Ref.String()), - }, nil - } + dstProviders, err := s.findProviders(ctx, req.Destination) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "move dst="+req.Destination.String(), err), + }, nil + } - if srcStatRes.Status.Code != rpc.Code_CODE_OK { - return &provider.MoveResponse{ - Status: srcStatRes.Status, - }, nil + // if providers are not the same we do not implement cross storage move yet. + if len(srcProviders) != 1 || len(dstProviders) != 1 { + res := &provider.MoveResponse{ + Status: status.NewUnimplemented(ctx, nil, "gateway: cross storage copy not yet implemented"), } + return res, nil + } - dstStatReq := &provider.StatRequest{Ref: &provider.Reference{Path: dshareName}} - dstStatRes, err := s.stat(ctx, dstStatReq) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+srcStatReq.Ref.String()), - }, nil - } + srcProvider, dstProvider := srcProviders[0], dstProviders[0] - if dstStatRes.Status.Code != rpc.Code_CODE_OK { - return &provider.MoveResponse{ - Status: srcStatRes.Status, - }, nil + // if providers are not the same we do not implement cross storage copy yet. + if srcProvider.Address != dstProvider.Address { + res := &provider.MoveResponse{ + Status: status.NewUnimplemented(ctx, nil, "gateway: cross storage copy not yet implemented"), } - - srcRi, srcProtocol, err := s.checkRef(ctx, srcStatRes.Info) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+srcStatRes.Info.Target, err), - }, nil - } - - if srcProtocol == "webdav" { - err = s.webdavRefMove(ctx, dstStatRes.Info.Target, shareChild, dshareChild) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewInternal(ctx, err, "gateway: error moving resource on webdav host: "+p), - }, nil - } - return &provider.MoveResponse{ - Status: status.NewOK(ctx), - }, nil - } - dstRi, dstProtocol, err := s.checkRef(ctx, dstStatRes.Info) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+srcStatRes.Info.Target, err), - }, nil - } - - if dstProtocol == "webdav" { - err = s.webdavRefMove(ctx, dstStatRes.Info.Target, shareChild, dshareChild) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewInternal(ctx, err, "gateway: error moving resource on webdav host: "+p), - }, nil - } - return &provider.MoveResponse{ - Status: status.NewOK(ctx), - }, nil - } - - src := &provider.Reference{ - Path: path.Join(srcRi.Path, shareChild), - } - dst := &provider.Reference{ - Path: path.Join(dstRi.Path, dshareChild), - } - - req.Source = src - req.Destination = dst - - return s.move(ctx, req) - } - - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "move", errtypes.BadRequest("gateway: move called on unknown path: "+p)), - }, nil -} - -func (s *svc) move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { - srcProviders, err := s.findProviders(ctx, req.Source) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "move src="+req.Source.String(), err), - }, nil - } - - dstProviders, err := s.findProviders(ctx, req.Destination) - if err != nil { - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "move dst="+req.Destination.String(), err), - }, nil - } - - // if providers are not the same we do not implement cross storage move yet. - if len(srcProviders) != 1 || len(dstProviders) != 1 { - res := &provider.MoveResponse{ - Status: status.NewUnimplemented(ctx, nil, "gateway: cross storage copy not yet implemented"), - } - return res, nil - } - - srcProvider, dstProvider := srcProviders[0], dstProviders[0] - - // if providers are not the same we do not implement cross storage copy yet. - if srcProvider.Address != dstProvider.Address { - res := &provider.MoveResponse{ - Status: status.NewUnimplemented(ctx, nil, "gateway: cross storage copy not yet implemented"), - } - return res, nil - } + return res, nil + } c, err := s.getStorageProviderClient(ctx, srcProvider) if err != nil { @@ -1199,23 +643,6 @@ func (s *svc) statHome(ctx context.Context) (*provider.StatResponse, error) { }, nil } - statSharedFolder, err := s.statSharesFolder(ctx) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating shares folder"), - }, nil - } - if statSharedFolder.Status.Code != rpc.Code_CODE_OK { - // If shares folder is not found, skip updating the etag - if statSharedFolder.Status.Code == rpc.Code_CODE_NOT_FOUND { - return statRes, nil - } - // otherwise return stat of share folder - return &provider.StatResponse{ - Status: statSharedFolder.Status, - }, nil - } - if etagIface, err := s.etagCache.Get(statRes.Info.Owner.OpaqueId + ":" + statRes.Info.Path); err == nil { resMtime := utils.TSToTime(statRes.Info.Mtime) resEtag := etagIface.(etagWithTS) @@ -1223,56 +650,10 @@ func (s *svc) statHome(ctx context.Context) (*provider.StatResponse, error) { if resMtime.Before(resEtag.Timestamp) { statRes.Info.Etag = resEtag.Etag } - } else { - statRes.Info.Etag = etag.GenerateEtagFromResources(statRes.Info, []*provider.ResourceInfo{statSharedFolder.Info}) - if s.c.EtagCacheTTL > 0 { - _ = s.etagCache.Set(statRes.Info.Owner.OpaqueId+":"+statRes.Info.Path, etagWithTS{statRes.Info.Etag, time.Now()}) - } - } - - return statRes, nil -} - -func (s *svc) statSharesFolder(ctx context.Context) (*provider.StatResponse, error) { - statRes, err := s.stat(ctx, &provider.StatRequest{Ref: &provider.Reference{Path: s.getSharedFolder(ctx)}}) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating shares folder"), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: statRes.Status, - }, nil - } - - lsRes, err := s.listSharesFolder(ctx) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing shares folder"), - }, nil - } - if lsRes.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: lsRes.Status, - }, nil + } else if s.c.EtagCacheTTL > 0 { + _ = s.etagCache.Set(statRes.Info.Owner.OpaqueId+":"+statRes.Info.Path, etagWithTS{statRes.Info.Etag, time.Now()}) } - if etagIface, err := s.etagCache.Get(statRes.Info.Owner.OpaqueId + ":" + statRes.Info.Path); err == nil { - resMtime := utils.TSToTime(statRes.Info.Mtime) - resEtag := etagIface.(etagWithTS) - // Use the updated etag if the shares folder has been modified, i.e., a new - // reference has been created. - if resMtime.Before(resEtag.Timestamp) { - statRes.Info.Etag = resEtag.Etag - } - } else { - statRes.Info.Etag = etag.GenerateEtagFromResources(statRes.Info, lsRes.Infos) - if s.c.EtagCacheTTL > 0 { - _ = s.etagCache.Set(statRes.Info.Owner.OpaqueId+":"+statRes.Info.Path, etagWithTS{statRes.Info.Etag, time.Now()}) - } - } return statRes, nil } @@ -1292,11 +673,37 @@ func (s *svc) stat(ctx context.Context, req *provider.StatRequest) (*provider.St Status: status.NewInternal(ctx, err, "error connecting to storage provider="+providers[0].Address), }, nil } - rsp, err := c.Stat(ctx, req) - if err != nil || rsp.Status.Code != rpc.Code_CODE_OK { - return rsp, err + + res, err := c.Stat(ctx, req) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "error connecting to storage provider="+providers[0].Address), + }, nil + } + + embeddedMounts := s.findEmbeddedMounts(resPath) + if len(embeddedMounts) > 0 { + etagHash := md5.New() + if res.Info != nil { + _, _ = io.WriteString(etagHash, res.Info.Etag) + } + for _, child := range embeddedMounts { + childStatRes, err := s.stat(ctx, &provider.StatRequest{Ref: &provider.Reference{Path: child}}) + if err != nil { + return &provider.StatResponse{ + Status: status.NewStatusFromErrType(ctx, "stat ref: "+req.Ref.String(), err), + }, nil + } + _, _ = io.WriteString(etagHash, childStatRes.Info.Etag) + } + + if res.Info == nil { + res.Info = &provider.ResourceInfo{} + } + res.Info.Etag = fmt.Sprintf("%x", etagHash.Sum(nil)) } - return rsp, nil + + return res, nil } return s.statAcrossProviders(ctx, req, providers) @@ -1383,190 +790,7 @@ func (s *svc) Stat(ctx context.Context, req *provider.StatRequest) (*provider.St return s.statHome(ctx) } - if s.isSharedFolder(ctx, p) { - return s.statSharesFolder(ctx) - } - - if !s.inSharedFolder(ctx, p) { - return s.stat(ctx, req) - } - - // we need to provide the info of the target, not the reference. - if s.isShareName(ctx, p) { - statRes, err := s.stat(ctx, req) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+req.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.StatResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - ri, err = s.webdavRefStat(ctx, statRes.Info.Target) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error resolving webdav reference: "+p), - }, nil - } - } - - // we need to make sure we don't expose the reference target in the resource - // information. For example, if requests comes to: /home/MyShares/photos and photos - // is reference to /user/peter/Holidays/photos, we need to still return to the user - // /home/MyShares/photos - orgPath := statRes.Info.Path - statRes.Info = ri - statRes.Info.Path = orgPath - return statRes, nil - - } - - if s.isShareChild(ctx, p) { - shareName, shareChild := s.splitShare(ctx, p) - - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.StatResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - ri, err = s.webdavRefStat(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error resolving webdav reference: "+p), - }, nil - } - ri.Path = p - return &provider.StatResponse{ - Status: status.NewOK(ctx), - Info: ri, - }, nil - } - - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - res, err := s.stat(ctx, req) - if err != nil { - return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+req.Ref.String()), - }, nil - } - if res.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: res.Status, - }, nil - } - - // we need to make sure we don't expose the reference target in the resource - // information. - res.Info.Path = p - return res, nil - } - - panic("gateway: stating an unknown path:" + p) -} - -func (s *svc) checkRef(ctx context.Context, ri *provider.ResourceInfo) (*provider.ResourceInfo, string, error) { - if ri.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { - panic("gateway: calling checkRef on a non reference type:" + ri.String()) - } - - // reference types MUST have a target resource id. - if ri.Target == "" { - err := errtypes.BadRequest("gateway: ref target is an empty uri") - return nil, "", err - } - - uri, err := url.Parse(ri.Target) - if err != nil { - return nil, "", errors.Wrapf(err, "gateway: error parsing target uri: %s", ri.Target) - } - - switch uri.Scheme { - case "cs3": - ref, err := s.handleCS3Ref(ctx, uri.Opaque) - return ref, "cs3", err - case "webdav": - return nil, "webdav", nil - default: - err := errtypes.BadRequest("gateway: no reference handler for scheme: " + uri.Scheme) - return nil, "", err - } -} - -func (s *svc) handleCS3Ref(ctx context.Context, opaque string) (*provider.ResourceInfo, error) { - // a cs3 ref has the following layout: / - parts := strings.SplitN(opaque, "/", 2) - if len(parts) < 2 { - err := errtypes.BadRequest("gateway: cs3 ref does not follow the layout storageid/opaqueid:" + opaque) - return nil, err - } - - // we could call here the Stat method again, but that is calling for problems in case - // there is a loop of targets pointing to targets, so better avoid it. - - req := &provider.StatRequest{ - Ref: &provider.Reference{ - ResourceId: &provider.ResourceId{ - StorageId: parts[0], - OpaqueId: parts[1], - }, - }, - } - res, err := s.stat(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "gateway: error calling stat") - } - - if res.Status.Code != rpc.Code_CODE_OK { - switch res.Status.Code { - case rpc.Code_CODE_NOT_FOUND: - return nil, errtypes.NotFound(req.Ref.String()) - case rpc.Code_CODE_PERMISSION_DENIED: - return nil, errtypes.PermissionDenied(req.Ref.String()) - case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE: - return nil, errtypes.BadRequest(req.Ref.String()) - case rpc.Code_CODE_UNIMPLEMENTED: - return nil, errtypes.NotSupported(req.Ref.String()) - default: - return nil, errtypes.InternalError("gateway: error stating target reference") - } - } - - if res.Info.Type == provider.ResourceType_RESOURCE_TYPE_REFERENCE { - err := errtypes.BadRequest("gateway: error the target of a reference cannot be another reference") - return nil, err - } - - return res.Info, nil + return s.stat(ctx, req) } func (s *svc) ListContainerStream(_ *provider.ListContainerStreamRequest, _ gateway.GatewayAPI_ListContainerStreamServer) error { @@ -1589,64 +813,6 @@ func (s *svc) listHome(ctx context.Context, req *provider.ListContainerRequest) }, nil } - for i := range lcr.Infos { - if s.isSharedFolder(ctx, lcr.Infos[i].GetPath()) { - statSharedFolder, err := s.statSharesFolder(ctx) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating shares folder"), - }, nil - } - if statSharedFolder.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: statSharedFolder.Status, - }, nil - } - lcr.Infos[i] = statSharedFolder.Info - break - } - } - - return lcr, nil -} - -func (s *svc) listSharesFolder(ctx context.Context) (*provider.ListContainerResponse, error) { - lcr, err := s.listContainer(ctx, &provider.ListContainerRequest{Ref: &provider.Reference{Path: s.getSharedFolder(ctx)}}) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing shared folder"), - }, nil - } - if lcr.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: lcr.Status, - }, nil - } - checkedInfos := make([]*provider.ResourceInfo, 0) - for i := range lcr.Infos { - info, protocol, err := s.checkRef(ctx, lcr.Infos[i]) - if err != nil { - // create status to log the proper messages - // this might arise when the shared resource has been moved to the recycle bin - // this might arise when the resource was unshared, but the share reference was not removed - status.NewStatusFromErrType(ctx, "error resolving reference "+lcr.Infos[i].Target, err) - // continue on errors so the user can see a list of the working shares - continue - } - - if protocol == "webdav" { - info, err = s.webdavRefStat(ctx, lcr.Infos[i].Target) - if err != nil { - // Might be the case that the webdav token has expired - continue - } - } - - info.Path = lcr.Infos[i].Path - checkedInfos = append(checkedInfos, info) - } - lcr.Infos = checkedInfos - return lcr, nil } @@ -1707,6 +873,27 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ infos = append(infos, inf) } + // Inject mountpoints if they do not exist on disk + embeddedMounts := s.findEmbeddedMounts(path.Clean(req.Ref.GetPath())) + for _, mount := range embeddedMounts { + for _, info := range infos { + if info.Path == mount { + continue + } + } + infos = append(infos, + &provider.ResourceInfo{ + Id: &provider.ResourceId{ + StorageId: "/", + OpaqueId: uuid.New().String(), + }, + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Etag: uuid.New().String(), + Path: mount, + Size: 0, + }) + } + return &provider.ListContainerResponse{ Status: status.NewOK(ctx), Infos: infos, @@ -1758,8 +945,6 @@ func (s *svc) listContainerOnProvider(ctx context.Context, req *provider.ListCon } func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { - log := appctx.GetLogger(ctx) - if utils.IsRelativeReference(req.Ref) { return s.listContainer(ctx, req) } @@ -1775,168 +960,7 @@ func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequ return s.listHome(ctx, req) } - if s.isSharedFolder(ctx, p) { - return s.listSharesFolder(ctx) - } - - if !s.inSharedFolder(ctx, p) { - return s.listContainer(ctx, req) - } - - // we need to provide the info of the target, not the reference. - if s.isShareName(ctx, p) { - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: p}} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating share:"+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - infos, err := s.webdavRefLs(ctx, statRes.Info.Target) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing webdav reference: "+p), - }, nil - } - - for _, info := range infos { - base := path.Base(info.Path) - info.Path = path.Join(p, base) - } - return &provider.ListContainerResponse{ - Status: status.NewOK(ctx), - Infos: infos, - }, nil - } - - if ri.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { - err := errtypes.NotSupported("gateway: list container: cannot list non-container type:" + ri.Path) - log.Err(err).Msg("gateway: error listing") - return &provider.ListContainerResponse{ - Status: status.NewInvalidArg(ctx, "resource is not a container"), - }, nil - } - - newReq := &provider.ListContainerRequest{ - Ref: &provider.Reference{Path: ri.Path}, - ArbitraryMetadataKeys: req.ArbitraryMetadataKeys, - } - newRes, err := s.listContainer(ctx, newReq) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing "+newReq.Ref.String()), - }, nil - } - - if newRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: newRes.Status, - }, nil - } - - // paths needs to be converted - for _, info := range newRes.Infos { - base := path.Base(info.Path) - info.Path = path.Join(p, base) - } - - return newRes, nil - - } - - if s.isShareChild(ctx, p) { - shareName, shareChild := s.splitShare(ctx, p) - - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - statRes, err := s.stat(ctx, statReq) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating share child "+statReq.Ref.String()), - }, nil - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: statRes.Status, - }, nil - } - - ri, protocol, err := s.checkRef(ctx, statRes.Info) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), - }, nil - } - - if protocol == "webdav" { - infos, err := s.webdavRefLs(ctx, statRes.Info.Target, shareChild) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing webdav reference: "+p), - }, nil - } - - for _, info := range infos { - base := path.Base(info.Path) - info.Path = path.Join(shareName, shareChild, base) - } - return &provider.ListContainerResponse{ - Status: status.NewOK(ctx), - Infos: infos, - }, nil - } - - if ri.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { - err := errtypes.NotSupported("gateway: list container: cannot list non-container type:" + ri.Path) - log.Err(err).Msg("gateway: error listing") - return &provider.ListContainerResponse{ - Status: status.NewInvalidArg(ctx, "resource is not a container"), - }, nil - } - - newReq := &provider.ListContainerRequest{ - Ref: &provider.Reference{Path: path.Join(ri.Path, shareChild)}, - ArbitraryMetadataKeys: req.ArbitraryMetadataKeys, - } - newRes, err := s.listContainer(ctx, newReq) - if err != nil { - return &provider.ListContainerResponse{ - Status: status.NewInternal(ctx, err, "gateway: error listing "+newReq.Ref.String()), - }, nil - } - - if newRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: newRes.Status, - }, nil - } - - // paths needs to be converted - for _, info := range newRes.Infos { - base := path.Base(info.Path) - info.Path = path.Join(shareName, shareChild, base) - } - - return newRes, nil - - } - - panic("gateway: stating an unknown path:" + p) + return s.listContainer(ctx, req) } func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...string) (string, *rpc.Status) { @@ -1961,75 +985,6 @@ func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...stri return "", &rpc.Status{Code: rpc.Code_CODE_INTERNAL} } -// /home/MyShares/ -func (s *svc) isSharedFolder(ctx context.Context, p string) bool { - return s.split(ctx, p, 2) -} - -// /home/MyShares/photos/ -func (s *svc) isShareName(ctx context.Context, p string) bool { - return s.split(ctx, p, 3) -} - -// /home/MyShares/photos/Ibiza/beach.png -func (s *svc) isShareChild(ctx context.Context, p string) bool { - return s.split(ctx, p, 4) -} - -// always validate that the path contains the share folder -// split cannot be called with i<2 -func (s *svc) split(ctx context.Context, p string, i int) bool { - log := appctx.GetLogger(ctx) - if i < 2 { - panic("split called with i < 2") - } - - parts := s.splitPath(ctx, p) - - // validate that we have always at least two elements - if len(parts) < 2 { - return false - } - - // validate the share folder is always the second element, first element is always the hardcoded value of "home" - if parts[1] != s.c.ShareFolder { - log.Debug().Msgf("gateway: split: parts[1]:%+v != shareFolder:%+v", parts[1], s.c.ShareFolder) - return false - } - - log.Debug().Msgf("gateway: split: path:%+v parts:%+v shareFolder:%+v", p, parts, s.c.ShareFolder) - - if len(parts) == i && parts[i-1] != "" { - return true - } - - return false -} - -// path must contain a share path with share children, if not it will panic. -// should be called after checking isShareChild == true -func (s *svc) splitShare(ctx context.Context, p string) (string, string) { - parts := s.splitPath(ctx, p) - if len(parts) != 4 { - panic("gateway: path for splitShare does not contain 4 elements:" + p) - } - - shareName := path.Join("/", parts[0], parts[1], parts[2]) - shareChild := path.Join("/", parts[3]) - return shareName, shareChild -} - -func (s *svc) splitPath(_ context.Context, p string) []string { - p = strings.Trim(p, "/") - return strings.SplitN(p, "/", 4) // ["home", "MyShares", "photos", "Ibiza/beach.png"] -} - -func (s *svc) getSharedFolder(ctx context.Context) string { - home := s.getHome(ctx) - shareFolder := path.Join(home, s.c.ShareFolder) - return shareFolder -} - func (s *svc) CreateSymlink(ctx context.Context, req *provider.CreateSymlinkRequest) (*provider.CreateSymlinkResponse, error) { return &provider.CreateSymlinkResponse{ Status: status.NewUnimplemented(ctx, errtypes.NotSupported("CreateSymlink not implemented"), "CreateSymlink not implemented"), @@ -2161,6 +1116,19 @@ func (s *svc) getStorageProviderClient(_ context.Context, p *registry.ProviderIn return c, nil } +func (s *svc) findEmbeddedMounts(basePath string) []string { + if basePath == "" { + return []string{} + } + mounts := []string{} + for mountPath := range s.c.StorageRules { + if strings.HasPrefix(mountPath, basePath) && mountPath != basePath { + mounts = append(mounts, mountPath) + } + } + return mounts +} + func (s *svc) findProviders(ctx context.Context, ref *provider.Reference) ([]*registry.ProviderInfo, error) { c, err := pool.GetStorageRegistryClient(s.c.StorageRegistryEndpoint) if err != nil { diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index 7511e0a581..b6950d9c65 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -23,6 +23,8 @@ import ( "fmt" "path" + rtrace "github.com/cs3org/reva/pkg/trace" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -37,11 +39,6 @@ import ( // TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled. func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) { - - if s.isSharedFolder(ctx, req.ResourceInfo.GetPath()) { - return nil, errtypes.AlreadyExists("gateway: can't share the share folder itself") - } - c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint) if err != nil { return &collaboration.CreateShareResponse{ @@ -275,7 +272,9 @@ func (s *svc) GetReceivedShare(ctx context.Context, req *collaboration.GetReceiv // 1) if received share is mounted: we also do a rename in the storage // 2) if received share is not mounted: we only rename in user share provider. func (s *svc) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) { - log := appctx.GetLogger(ctx) + t := rtrace.Provider.Tracer("reva") + ctx, span := t.Start(ctx, "Gateway.UpdateReceivedShare") + defer span.End() // sanity checks switch { @@ -305,64 +304,7 @@ func (s *svc) UpdateReceivedShare(ctx context.Context, req *collaboration.Update }, nil } - res, err := c.UpdateReceivedShare(ctx, req) - if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare") - return &collaboration.UpdateReceivedShareResponse{ - Status: &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - }, - }, nil - } - - // error failing to update share state. - if res.Status.Code != rpc.Code_CODE_OK { - return res, nil - } - - // if we don't need to create/delete references then we return early. - if !s.c.CommitShareToStorageRef { - return res, nil - } - - // check if we have a resource id in the update response that we can use to update references - if res.GetShare().GetShare().GetResourceId() == nil { - log.Err(err).Msg("gateway: UpdateReceivedShare must return a ResourceId") - return &collaboration.UpdateReceivedShareResponse{ - Status: &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - }, - }, 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 collaboration.ShareState_SHARE_STATE_ACCEPTED: - rpcStatus := s.createReference(ctx, res.GetShare().GetShare().GetResourceId()) - if rpcStatus.Code != rpc.Code_CODE_OK { - return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil - } - case collaboration.ShareState_SHARE_STATE_REJECTED: - rpcStatus := s.removeReference(ctx, res.GetShare().GetShare().ResourceId) - if rpcStatus.Code != rpc.Code_CODE_OK && rpcStatus.Code != rpc.Code_CODE_NOT_FOUND { - return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil - } - } - case "mount_point": - // TODO(labkode): implementing updating mount point - err = errtypes.NotSupported("gateway: update of mount point is not yet implemented") - return &collaboration.UpdateReceivedShareResponse{ - Status: status.NewUnimplemented(ctx, err, "error updating received share"), - }, nil - default: - return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported") - } - } - return res, nil + return c.UpdateReceivedShare(ctx, req) } func (s *svc) removeReference(ctx context.Context, resourceID *provider.ResourceId) *rpc.Status { @@ -491,7 +433,7 @@ func (s *svc) createReference(ctx context.Context, resourceID *provider.Resource TargetUri: fmt.Sprintf("cs3:%s/%s", resourceID.GetStorageId(), resourceID.GetOpaqueId()), } - c, err = s.findByPath(ctx, refPath) + c, err = s.findByPath(ctx, homeRes.Path) if err != nil { if _, ok := err.(errtypes.IsNotFound); ok { return status.NewNotFound(ctx, "storage provider not found") diff --git a/internal/grpc/services/gateway/webdavstorageprovider.go b/internal/grpc/services/gateway/webdavstorageprovider.go index 8d98e8688c..10782dba91 100644 --- a/internal/grpc/services/gateway/webdavstorageprovider.go +++ b/internal/grpc/services/gateway/webdavstorageprovider.go @@ -20,18 +20,11 @@ package gateway import ( "context" - "fmt" "net/url" "path" - "strings" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/pkg/errors" - "github.com/studio-b12/gowebdav" ) type webdavEndpoint struct { @@ -40,178 +33,6 @@ type webdavEndpoint struct { token string } -func (s *svc) webdavRefStat(ctx context.Context, targetURL string, nameQueries ...string) (*provider.ResourceInfo, error) { - targetURL, err := appendNameQuery(targetURL, nameQueries...) - if err != nil { - return nil, err - } - - ep, err := s.extractEndpointInfo(ctx, targetURL) - if err != nil { - return nil, err - } - webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) - if err != nil { - return nil, err - } - - c := gowebdav.NewClient(webdavEP, "", "") - c.SetHeader(ctxpkg.TokenHeader, ep.token) - - // TODO(ishank011): We need to call PROPFIND ourselves as we need to retrieve - // ownloud-specific fields to get the resource ID and permissions. - info, err := c.Stat(ep.filePath) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("gateway: error statting %s at the webdav endpoint: %s", ep.filePath, webdavEP)) - } - return normalize(info.(*gowebdav.File)), nil -} - -func (s *svc) webdavRefLs(ctx context.Context, targetURL string, nameQueries ...string) ([]*provider.ResourceInfo, error) { - targetURL, err := appendNameQuery(targetURL, nameQueries...) - if err != nil { - return nil, err - } - - ep, err := s.extractEndpointInfo(ctx, targetURL) - if err != nil { - return nil, err - } - webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) - if err != nil { - return nil, err - } - - c := gowebdav.NewClient(webdavEP, "", "") - c.SetHeader(ctxpkg.TokenHeader, ep.token) - - // TODO(ishank011): We need to call PROPFIND ourselves as we need to retrieve - // ownloud-specific fields to get the resource ID and permissions. - infos, err := c.ReadDir(ep.filePath) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("gateway: error listing %s at the webdav endpoint: %s", ep.filePath, webdavEP)) - } - - mds := []*provider.ResourceInfo{} - for _, fi := range infos { - info := fi.(gowebdav.File) - mds = append(mds, normalize(&info)) - } - return mds, nil -} - -func (s *svc) webdavRefMkdir(ctx context.Context, targetURL string, nameQueries ...string) error { - targetURL, err := appendNameQuery(targetURL, nameQueries...) - if err != nil { - return err - } - - ep, err := s.extractEndpointInfo(ctx, targetURL) - if err != nil { - return err - } - webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) - if err != nil { - return err - } - - c := gowebdav.NewClient(webdavEP, "", "") - c.SetHeader(ctxpkg.TokenHeader, ep.token) - - err = c.Mkdir(ep.filePath, 0700) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("gateway: error creating dir %s at the webdav endpoint: %s", ep.filePath, webdavEP)) - } - return nil -} - -func (s *svc) webdavRefMove(ctx context.Context, targetURL, src, destination string) error { - srcURL, err := appendNameQuery(targetURL, src) - if err != nil { - return err - } - srcEP, err := s.extractEndpointInfo(ctx, srcURL) - if err != nil { - return err - } - srcWebdavEP, err := s.getWebdavEndpoint(ctx, srcEP.endpoint) - if err != nil { - return err - } - - destURL, err := appendNameQuery(targetURL, destination) - if err != nil { - return err - } - destEP, err := s.extractEndpointInfo(ctx, destURL) - if err != nil { - return err - } - - c := gowebdav.NewClient(srcWebdavEP, "", "") - c.SetHeader(ctxpkg.TokenHeader, srcEP.token) - - err = c.Rename(srcEP.filePath, destEP.filePath, true) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("gateway: error renaming %s to %s at the webdav endpoint: %s", srcEP.filePath, destEP.filePath, srcWebdavEP)) - } - return nil -} - -func (s *svc) webdavRefDelete(ctx context.Context, targetURL string, nameQueries ...string) error { - targetURL, err := appendNameQuery(targetURL, nameQueries...) - if err != nil { - return err - } - - ep, err := s.extractEndpointInfo(ctx, targetURL) - if err != nil { - return err - } - webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) - if err != nil { - return err - } - - c := gowebdav.NewClient(webdavEP, "", "") - c.SetHeader(ctxpkg.TokenHeader, ep.token) - - err = c.Remove(ep.filePath) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("gateway: error removing %s at the webdav endpoint: %s", ep.filePath, webdavEP)) - } - return nil -} - -func (s *svc) webdavRefTransferEndpoint(ctx context.Context, targetURL string, nameQueries ...string) (string, *types.Opaque, error) { - targetURL, err := appendNameQuery(targetURL, nameQueries...) - if err != nil { - return "", nil, err - } - - ep, err := s.extractEndpointInfo(ctx, targetURL) - if err != nil { - return "", nil, err - } - webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) - if err != nil { - return "", nil, err - } - - return webdavEP, &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "webdav-file-path": { - Decoder: "plain", - Value: []byte(ep.filePath), - }, - "webdav-token": { - Decoder: "plain", - Value: []byte(ep.token), - }, - }, - }, nil -} - func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) { if targetURL == "" { return nil, errtypes.BadRequest("gateway: ref target is an empty uri") @@ -237,42 +58,6 @@ func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webda }, nil } -func (s *svc) getWebdavEndpoint(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.Endpoint.Path, nil - } - } - return "", errtypes.NotFound(domain) -} - -func normalize(info *gowebdav.File) *provider.ResourceInfo { - return &provider.ResourceInfo{ - // TODO(ishank011): Add Id, PermissionSet, Owner - Path: info.Path(), - Type: getResourceType(info.IsDir()), - Etag: info.ETag(), - MimeType: info.ContentType(), - Size: uint64(info.Size()), - Mtime: &types.Timestamp{ - Seconds: uint64(info.ModTime().Unix()), - }, - } -} - -func getResourceType(isDir bool) provider.ResourceType { - if isDir { - return provider.ResourceType_RESOURCE_TYPE_CONTAINER - } - return provider.ResourceType_RESOURCE_TYPE_FILE -} - func appendNameQuery(targetURL string, nameQueries ...string) (string, error) { uri, err := url.Parse(targetURL) if err != nil { diff --git a/internal/grpc/services/loader/loader.go b/internal/grpc/services/loader/loader.go index 118eeed39e..4b72259cdf 100644 --- a/internal/grpc/services/loader/loader.go +++ b/internal/grpc/services/loader/loader.go @@ -36,6 +36,7 @@ import ( _ "github.com/cs3org/reva/internal/grpc/services/preferences" _ "github.com/cs3org/reva/internal/grpc/services/publicshareprovider" _ "github.com/cs3org/reva/internal/grpc/services/publicstorageprovider" + _ "github.com/cs3org/reva/internal/grpc/services/sharesstorageprovider" _ "github.com/cs3org/reva/internal/grpc/services/storageprovider" _ "github.com/cs3org/reva/internal/grpc/services/storageregistry" _ "github.com/cs3org/reva/internal/grpc/services/userprovider" diff --git a/internal/grpc/services/sharesstorageprovider/mocks/GatewayClient.go b/internal/grpc/services/sharesstorageprovider/mocks/GatewayClient.go new file mode 100644 index 0000000000..3ebf3a3c1a --- /dev/null +++ b/internal/grpc/services/sharesstorageprovider/mocks/GatewayClient.go @@ -0,0 +1,367 @@ +// 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. + +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +// GatewayClient is an autogenerated mock type for the GatewayClient type +type GatewayClient struct { + mock.Mock +} + +// CreateContainer provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) CreateContainer(ctx context.Context, in *providerv1beta1.CreateContainerRequest, opts ...grpc.CallOption) (*providerv1beta1.CreateContainerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.CreateContainerResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.CreateContainerRequest, ...grpc.CallOption) *providerv1beta1.CreateContainerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.CreateContainerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.CreateContainerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Delete(ctx context.Context, in *providerv1beta1.DeleteRequest, opts ...grpc.CallOption) (*providerv1beta1.DeleteResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.DeleteResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.DeleteRequest, ...grpc.CallOption) *providerv1beta1.DeleteResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.DeleteResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.DeleteRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitiateFileDownload provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) InitiateFileDownload(ctx context.Context, req *providerv1beta1.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*gatewayv1beta1.InitiateFileDownloadResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gatewayv1beta1.InitiateFileDownloadResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.InitiateFileDownloadRequest, ...grpc.CallOption) *gatewayv1beta1.InitiateFileDownloadResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gatewayv1beta1.InitiateFileDownloadResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.InitiateFileDownloadRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitiateFileUpload provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) InitiateFileUpload(ctx context.Context, req *providerv1beta1.InitiateFileUploadRequest, opts ...grpc.CallOption) (*gatewayv1beta1.InitiateFileUploadResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gatewayv1beta1.InitiateFileUploadResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.InitiateFileUploadRequest, ...grpc.CallOption) *gatewayv1beta1.InitiateFileUploadResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gatewayv1beta1.InitiateFileUploadResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.InitiateFileUploadRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListContainer provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) ListContainer(ctx context.Context, in *providerv1beta1.ListContainerRequest, opts ...grpc.CallOption) (*providerv1beta1.ListContainerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.ListContainerResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) *providerv1beta1.ListContainerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.ListContainerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListFileVersions provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) ListFileVersions(ctx context.Context, req *providerv1beta1.ListFileVersionsRequest, opts ...grpc.CallOption) (*providerv1beta1.ListFileVersionsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.ListFileVersionsResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListFileVersionsRequest, ...grpc.CallOption) *providerv1beta1.ListFileVersionsResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.ListFileVersionsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListFileVersionsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Move provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Move(ctx context.Context, in *providerv1beta1.MoveRequest, opts ...grpc.CallOption) (*providerv1beta1.MoveResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.MoveResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.MoveRequest, ...grpc.CallOption) *providerv1beta1.MoveResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.MoveResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.MoveRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RestoreFileVersion provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) RestoreFileVersion(ctx context.Context, req *providerv1beta1.RestoreFileVersionRequest, opts ...grpc.CallOption) (*providerv1beta1.RestoreFileVersionResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.RestoreFileVersionResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.RestoreFileVersionRequest, ...grpc.CallOption) *providerv1beta1.RestoreFileVersionResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.RestoreFileVersionResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.RestoreFileVersionRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetArbitraryMetadata provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) SetArbitraryMetadata(ctx context.Context, req *providerv1beta1.SetArbitraryMetadataRequest, opts ...grpc.CallOption) (*providerv1beta1.SetArbitraryMetadataResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.SetArbitraryMetadataResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.SetArbitraryMetadataRequest, ...grpc.CallOption) *providerv1beta1.SetArbitraryMetadataResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.SetArbitraryMetadataResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.SetArbitraryMetadataRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Stat provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Stat(ctx context.Context, in *providerv1beta1.StatRequest, opts ...grpc.CallOption) (*providerv1beta1.StatResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.StatResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) *providerv1beta1.StatResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.StatResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnsetArbitraryMetadata provides a mock function with given fields: ctx, req, opts +func (_m *GatewayClient) UnsetArbitraryMetadata(ctx context.Context, req *providerv1beta1.UnsetArbitraryMetadataRequest, opts ...grpc.CallOption) (*providerv1beta1.UnsetArbitraryMetadataResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.UnsetArbitraryMetadataResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.UnsetArbitraryMetadataRequest, ...grpc.CallOption) *providerv1beta1.UnsetArbitraryMetadataResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.UnsetArbitraryMetadataResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.UnsetArbitraryMetadataRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/internal/grpc/services/sharesstorageprovider/mocks/SharesProviderClient.go b/internal/grpc/services/sharesstorageprovider/mocks/SharesProviderClient.go new file mode 100644 index 0000000000..747d497de1 --- /dev/null +++ b/internal/grpc/services/sharesstorageprovider/mocks/SharesProviderClient.go @@ -0,0 +1,96 @@ +// 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. + +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" +) + +// SharesProviderClient is an autogenerated mock type for the SharesProviderClient type +type SharesProviderClient struct { + mock.Mock +} + +// ListReceivedShares provides a mock function with given fields: ctx, req, opts +func (_m *SharesProviderClient) ListReceivedShares(ctx context.Context, req *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.ListReceivedSharesResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ListReceivedSharesRequest, ...grpc.CallOption) *collaborationv1beta1.ListReceivedSharesResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.ListReceivedSharesResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.ListReceivedSharesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateReceivedShare provides a mock function with given fields: ctx, req, opts +func (_m *SharesProviderClient) UpdateReceivedShare(ctx context.Context, req *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.UpdateReceivedShareResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.UpdateReceivedShareRequest, ...grpc.CallOption) *collaborationv1beta1.UpdateReceivedShareResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.UpdateReceivedShareResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.UpdateReceivedShareRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go new file mode 100644 index 0000000000..4a59ccbe80 --- /dev/null +++ b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go @@ -0,0 +1,980 @@ +// 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 sharesstorageprovider + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + revactx "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/rgrpc" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/storage/utils/etag" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +//go:generate mockery -name GatewayClient -name SharesProviderClient + +// GatewayClient describe the interface of a gateway client +type GatewayClient interface { + Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) + Move(ctx context.Context, in *provider.MoveRequest, opts ...grpc.CallOption) (*provider.MoveResponse, error) + Delete(ctx context.Context, in *provider.DeleteRequest, opts ...grpc.CallOption) (*provider.DeleteResponse, error) + CreateContainer(ctx context.Context, in *provider.CreateContainerRequest, opts ...grpc.CallOption) (*provider.CreateContainerResponse, error) + ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) + ListFileVersions(ctx context.Context, req *provider.ListFileVersionsRequest, opts ...grpc.CallOption) (*provider.ListFileVersionsResponse, error) + RestoreFileVersion(ctx context.Context, req *provider.RestoreFileVersionRequest, opts ...grpc.CallOption) (*provider.RestoreFileVersionResponse, error) + InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*gateway.InitiateFileDownloadResponse, error) + InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest, opts ...grpc.CallOption) (*gateway.InitiateFileUploadResponse, error) + SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.SetArbitraryMetadataResponse, error) + UnsetArbitraryMetadata(ctx context.Context, req *provider.UnsetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.UnsetArbitraryMetadataResponse, error) +} + +// SharesProviderClient provides methods for listing and modifying received shares +type SharesProviderClient interface { + ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaboration.ListReceivedSharesResponse, error) + UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaboration.UpdateReceivedShareResponse, error) +} + +func init() { + rgrpc.Register("sharesstorageprovider", NewDefault) +} + +type config struct { + MountPath string `mapstructure:"mount_path"` + GatewayAddr string `mapstructure:"gateway_addr"` + UserShareProviderEndpoint string `mapstructure:"usershareprovidersvc"` +} + +type service struct { + mountPath string + gateway GatewayClient + sharesProviderClient SharesProviderClient +} +type stattedReceivedShare struct { + Stat *provider.ResourceInfo + ReceivedShare *collaboration.ReceivedShare + AllReceivedShares []*collaboration.ReceivedShare +} + +type shareNotFoundError struct { + name string +} + +func (e *shareNotFoundError) Error() string { + return "Unknown share:" + e.name +} + +func isShareNotFoundError(e error) bool { + _, ok := e.(*shareNotFoundError) + return ok +} + +func (s *service) Close() error { + return nil +} + +func (s *service) UnprotectedEndpoints() []string { + return []string{} +} + +func (s *service) Register(ss *grpc.Server) { + provider.RegisterProviderAPIServer(ss, s) +} + +// NewDefault returns a new instance of the SharesStorageProvider service with default dependencies +func NewDefault(m map[string]interface{}, _ *grpc.Server) (rgrpc.Service, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + + gateway, err := pool.GetGatewayServiceClient(c.GatewayAddr) + if err != nil { + return nil, err + } + + client, err := pool.GetUserShareProviderClient(c.UserShareProviderEndpoint) + if err != nil { + return nil, errors.Wrap(err, "sharesstorageprovider: error getting UserShareProvider client") + } + + return New(c.MountPath, gateway, client) +} + +// New returns a new instance of the SharesStorageProvider service +func New(mountpath string, gateway GatewayClient, c SharesProviderClient) (rgrpc.Service, error) { + s := &service{ + mountPath: mountpath, + gateway: gateway, + sharesProviderClient: c, + } + return s, nil +} + +func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest) (*provider.SetArbitraryMetadataResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got SetArbitraryMetadata request") + + if reqShare == "" { + return &provider.SetArbitraryMetadataResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.SetArbitraryMetadataResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.SetArbitraryMetadataResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + gwres, err := s.gateway.SetArbitraryMetadata(ctx, &provider.SetArbitraryMetadataRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + ArbitraryMetadata: req.ArbitraryMetadata, + }) + + if err != nil { + return &provider.SetArbitraryMetadataResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling SetArbitraryMetadata"), + }, nil + } + + return gwres, nil +} + +func (s *service) UnsetArbitraryMetadata(ctx context.Context, req *provider.UnsetArbitraryMetadataRequest) (*provider.UnsetArbitraryMetadataResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got UnsetArbitraryMetadata request") + + if reqShare == "" { + return &provider.UnsetArbitraryMetadataResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.UnsetArbitraryMetadataResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.UnsetArbitraryMetadataResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.UnsetArbitraryMetadata(ctx, &provider.UnsetArbitraryMetadataRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + ArbitraryMetadataKeys: req.ArbitraryMetadataKeys, + }) + + if err != nil { + return &provider.UnsetArbitraryMetadataResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling UnsetArbitraryMetadata"), + }, nil + } + + return gwres, nil +} + +func (s *service) InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got InitiateFileDownload request") + + if reqShare == "" { + return &provider.InitiateFileDownloadResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.InitiateFileDownloadResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + if err != nil { + return &provider.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling InitiateFileDownload"), + }, nil + } + + if gwres.Status.Code != rpc.Code_CODE_OK { + return &provider.InitiateFileDownloadResponse{ + Status: gwres.Status, + }, nil + } + + protocols := []*provider.FileDownloadProtocol{} + for p := range gwres.Protocols { + if !strings.HasSuffix(gwres.Protocols[p].DownloadEndpoint, "/") { + gwres.Protocols[p].DownloadEndpoint += "/" + } + gwres.Protocols[p].DownloadEndpoint += gwres.Protocols[p].Token + + protocols = append(protocols, &provider.FileDownloadProtocol{ + Opaque: gwres.Protocols[p].Opaque, + Protocol: gwres.Protocols[p].Protocol, + DownloadEndpoint: gwres.Protocols[p].DownloadEndpoint, + Expose: true, // the gateway already has encoded the upload endpoint + }) + } + + return &provider.InitiateFileDownloadResponse{ + Status: gwres.Status, + Protocols: protocols, + }, nil +} + +func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got InitiateFileUpload request") + + if reqShare == "" { + return &provider.InitiateFileUploadResponse{ + Status: status.NewInvalidArg(ctx, "sharesstorageprovider: can not upload directly to the shares folder"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.InitiateFileUploadResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.InitiateFileUpload(ctx, &provider.InitiateFileUploadRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + Opaque: req.Opaque, + }) + if err != nil { + return &provider.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling InitiateFileDownload"), + }, nil + } + if gwres.Status.Code != rpc.Code_CODE_OK { + return &provider.InitiateFileUploadResponse{ + Status: gwres.Status, + }, nil + } + + protocols := []*provider.FileUploadProtocol{} + for p := range gwres.Protocols { + if !strings.HasSuffix(gwres.Protocols[p].UploadEndpoint, "/") { + gwres.Protocols[p].UploadEndpoint += "/" + } + gwres.Protocols[p].UploadEndpoint += gwres.Protocols[p].Token + + protocols = append(protocols, &provider.FileUploadProtocol{ + Opaque: gwres.Protocols[p].Opaque, + Protocol: gwres.Protocols[p].Protocol, + UploadEndpoint: gwres.Protocols[p].UploadEndpoint, + AvailableChecksums: gwres.Protocols[p].AvailableChecksums, + Expose: true, // the gateway already has encoded the upload endpoint + }) + } + + return &provider.InitiateFileUploadResponse{ + Status: gwres.Status, + Protocols: protocols, + }, nil +} + +func (s *service) GetPath(ctx context.Context, req *provider.GetPathRequest) (*provider.GetPathResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) GetHome(ctx context.Context, req *provider.GetHomeRequest) (*provider.GetHomeResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) CreateHome(ctx context.Context, req *provider.CreateHomeRequest) (*provider.CreateHomeResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got CreateContainer request") + + if reqShare == "" || reqPath == "" { + return &provider.CreateContainerResponse{ + Status: status.NewInvalid(ctx, "sharesstorageprovider: can not create top-level container"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.CreateContainerResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.CreateContainerResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.CreateContainer(ctx, &provider.CreateContainerRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + + if err != nil { + return &provider.CreateContainerResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling InitiateFileDownload"), + }, nil + } + + if gwres.Status.Code != rpc.Code_CODE_OK { + return &provider.CreateContainerResponse{ + Status: gwres.Status, + }, nil + } + + return gwres, nil +} + +func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got Delete request") + + if reqShare == "" { + return &provider.DeleteResponse{ + Status: status.NewInvalid(ctx, "sharesstorageprovider: can not delete top-level container"), + }, nil + } + + if reqPath == "" { + err := s.rejectReceivedShare(ctx, reqShare) + if err != nil { + return &provider.DeleteResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error rejecting share"), + }, nil + } + return &provider.DeleteResponse{ + Status: status.NewOK(ctx), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.DeleteResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.DeleteResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.Delete(ctx, &provider.DeleteRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + + if err != nil { + return &provider.DeleteResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling Delete"), + }, nil + } + + if gwres.Status.Code != rpc.Code_CODE_OK { + return &provider.DeleteResponse{ + Status: gwres.Status, + }, nil + } + + return gwres, nil +} + +func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { + reqShare, reqPath := s.resolvePath(req.Source.GetPath()) + destinationShare, destinationPath := s.resolvePath(req.Destination.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Interface("destinationPath", destinationPath). + Interface("destinationShare", destinationShare). + Msg("sharesstorageprovider: Got Move request") + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.MoveResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + if reqShare != destinationShare && reqPath == "" { + // Change the MountPoint of the share + stattedShare.ReceivedShare.MountPoint = &provider.Reference{Path: destinationShare} + + _, err = s.sharesProviderClient.UpdateReceivedShare(ctx, &collaboration.UpdateReceivedShareRequest{ + Share: stattedShare.ReceivedShare, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state", "mount_point"}}, + }) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: can not change mountpoint of share"), + }, nil + } + return &provider.MoveResponse{ + Status: status.NewOK(ctx), + }, nil + } + + dstStattedShare, err := s.statShare(ctx, destinationShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.MoveResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + if stattedShare.Stat.Id.StorageId != dstStattedShare.Stat.Id.StorageId { + return &provider.MoveResponse{ + Status: status.NewInvalid(ctx, "sharesstorageprovider: can not move between shares on different storages"), + }, nil + } + + gwres, err := s.gateway.Move(ctx, &provider.MoveRequest{ + Source: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + Destination: &provider.Reference{ + Path: filepath.Join(dstStattedShare.Stat.Path, destinationPath), + }, + }) + + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling Move"), + }, nil + } + + if gwres.Status.Code != rpc.Code_CODE_OK { + return &provider.MoveResponse{ + Status: gwres.Status, + }, nil + } + + return gwres, nil +} + +func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provider.StatResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got Stat request") + + _, ok := revactx.ContextGetUser(ctx) + if !ok { + return &provider.StatResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: shares requested for empty user"), + }, nil + } + + if reqShare != "" { + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.StatResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + res := &provider.StatResponse{ + Info: stattedShare.Stat, + Status: status.NewOK(ctx), + } + if reqPath != "" { + res, err = s.gateway.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error getting stat from gateway"), + }, nil + } + if res.Status.Code != rpc.Code_CODE_OK { + return res, nil + } + } + + origReqShare := filepath.Base(stattedShare.Stat.Path) + relPath := strings.SplitAfterN(res.Info.Path, origReqShare, 2)[1] + res.Info.Path = filepath.Join(s.mountPath, reqShare, relPath) + + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Interface("res", res). + Msg("sharesstorageprovider: Got Stat request") + + return res, nil + } + + shares, err := s.getReceivedShares(ctx) + if err != nil { + return nil, err + } + + res := &provider.StatResponse{ + Info: &provider.ResourceInfo{ + Path: filepath.Join(s.mountPath), + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + }, + } + childInfos := []*provider.ResourceInfo{} + for _, shares := range shares { + if shares.ReceivedShare.State != collaboration.ShareState_SHARE_STATE_ACCEPTED { + continue + } + + childInfos = append(childInfos, shares.Stat) + res.Info.Size += shares.Stat.Size + } + res.Status = status.NewOK(ctx) + res.Info.Etag = etag.GenerateEtagFromResources(res.Info, childInfos) + return res, nil +} + +func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, ss provider.ProviderAPI_ListContainerStreamServer) error { + return gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got ListContainer request") + + stattedShares, err := s.getReceivedShares(ctx) + if err != nil { + return nil, err + } + + res := &provider.ListContainerResponse{} + for name, stattedShare := range stattedShares { + if stattedShare.ReceivedShare.State != collaboration.ShareState_SHARE_STATE_ACCEPTED { + continue + } + // gwres, err := s.gateway.Stat(ctx, &provider.StatRequest{ + // Ref: &provider.Reference{f + // ResourceId: stattedShare.ReceivedShare.Share.ResourceId, + // }, + // }) + // if err != nil { + // return &provider.ListContainerResponse{ + // Status: status.NewInternal(ctx, err, "sharesstorageprovider: error getting stats from gateway"), + // }, nil + // } + // if gwres.Status.Code != rpc.Code_CODE_OK { + // appctx.GetLogger(ctx).Debug(). + // Interface("reqPath", reqPath). + // Interface("reqShare", reqShare). + // Interface("rss.Share", stattedShare.ReceivedShare.Share). + // Interface("gwres", gwres). + // Msg("sharesstorageprovider: Got non-ok ListContainerResponse response") + // continue + // } + + if reqShare != "" && (name == reqShare || (stattedShare.ReceivedShare.MountPoint != nil && stattedShare.ReceivedShare.MountPoint.Path == reqShare)) { + origReqShare := filepath.Base(stattedShare.Stat.Path) + gwListRes, err := s.gateway.ListContainer(ctx, &provider.ListContainerRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(filepath.Dir(stattedShare.Stat.Path), origReqShare, reqPath), + }, + }) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error getting listing from gateway"), + }, nil + } + for _, info := range gwListRes.Infos { + relPath := strings.SplitAfterN(info.Path, origReqShare, 2)[1] + info.Path = filepath.Join(s.mountPath, reqShare, relPath) + info.PermissionSet = stattedShare.Stat.PermissionSet + } + return gwListRes, nil + } else if reqShare == "" { + path := stattedShare.Stat.Path + if stattedShare.ReceivedShare.MountPoint != nil { + path = stattedShare.ReceivedShare.MountPoint.Path + } + stattedShare.Stat.Path = filepath.Join(s.mountPath, filepath.Base(path)) + res.Infos = append(res.Infos, stattedShare.Stat) + } + } + res.Status = status.NewOK(ctx) + + return res, nil +} +func (s *service) ListFileVersions(ctx context.Context, req *provider.ListFileVersionsRequest) (*provider.ListFileVersionsResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got ListFileVersions request") + + if reqShare == "" || reqPath == "" { + return &provider.ListFileVersionsResponse{ + Status: status.NewInvalid(ctx, "sharesstorageprovider: can not list versions of a share or share folder"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.ListFileVersionsResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.ListFileVersionsResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + + gwres, err := s.gateway.ListFileVersions(ctx, &provider.ListFileVersionsRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + + if err != nil { + return &provider.ListFileVersionsResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling ListFileVersions"), + }, nil + } + + return gwres, nil + +} + +func (s *service) RestoreFileVersion(ctx context.Context, req *provider.RestoreFileVersionRequest) (*provider.RestoreFileVersionResponse, error) { + reqShare, reqPath := s.resolvePath(req.Ref.GetPath()) + appctx.GetLogger(ctx).Debug(). + Interface("reqPath", reqPath). + Interface("reqShare", reqShare). + Msg("sharesstorageprovider: Got RestoreFileVersion request") + + if reqShare == "" || reqPath == "" { + return &provider.RestoreFileVersionResponse{ + Status: status.NewInvalid(ctx, "sharesstorageprovider: can not restore version of share or shares folder"), + }, nil + } + + stattedShare, err := s.statShare(ctx, reqShare) + if err != nil { + if isShareNotFoundError(err) { + return &provider.RestoreFileVersionResponse{ + Status: status.NewNotFound(ctx, "sharesstorageprovider: file not found"), + }, nil + } + return &provider.RestoreFileVersionResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error stating share"), + }, nil + } + gwres, err := s.gateway.RestoreFileVersion(ctx, &provider.RestoreFileVersionRequest{ + Ref: &provider.Reference{ + Path: filepath.Join(stattedShare.Stat.Path, reqPath), + }, + }) + + if err != nil { + return &provider.RestoreFileVersionResponse{ + Status: status.NewInternal(ctx, err, "sharesstorageprovider: error calling ListFileVersions"), + }, nil + } + + return gwres, nil +} + +func (s *service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss provider.ProviderAPI_ListRecycleStreamServer) error { + return gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) ListRecycle(ctx context.Context, req *provider.ListRecycleRequest) (*provider.ListRecycleResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) RestoreRecycleItem(ctx context.Context, req *provider.RestoreRecycleItemRequest) (*provider.RestoreRecycleItemResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRequest) (*provider.PurgeRecycleResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) ListGrants(ctx context.Context, req *provider.ListGrantsRequest) (*provider.ListGrantsResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) AddGrant(ctx context.Context, req *provider.AddGrantRequest) (*provider.AddGrantResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) DenyGrant(ctx context.Context, ref *provider.DenyGrantRequest) (*provider.DenyGrantResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) CreateReference(ctx context.Context, req *provider.CreateReferenceRequest) (*provider.CreateReferenceResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) CreateSymlink(ctx context.Context, req *provider.CreateSymlinkRequest) (*provider.CreateSymlinkResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) UpdateGrant(ctx context.Context, req *provider.UpdateGrantRequest) (*provider.UpdateGrantResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) RemoveGrant(ctx context.Context, req *provider.RemoveGrantRequest) (*provider.RemoveGrantResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (*provider.GetQuotaResponse, error) { + return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") +} + +func (s *service) resolvePath(path string) (string, string) { + // //share/path/to/something + parts := strings.SplitN(strings.TrimLeft(strings.TrimPrefix(path, s.mountPath), "/"), "/", 2) + var reqShare, reqPath string + if len(parts) >= 2 { + reqPath = parts[1] + } + if len(parts) >= 1 { + reqShare = parts[0] + } + return reqShare, reqPath +} + +func (s *service) statShare(ctx context.Context, share string) (*stattedReceivedShare, error) { + _, ok := revactx.ContextGetUser(ctx) + if !ok { + return nil, fmt.Errorf("sharesstorageprovider: shares requested for empty user") + } + + shares, err := s.getReceivedShares(ctx) + if err != nil { + return nil, fmt.Errorf("sharesstorageprovider: error getting received shares") + } + stattedShare, ok := shares[share] + if !ok { + for _, ss := range shares { + if ss.ReceivedShare.MountPoint != nil && ss.ReceivedShare.MountPoint.Path == share { + stattedShare, ok = ss, true + } + } + } + if !ok { + return nil, &shareNotFoundError{name: share} + } + return stattedShare, nil +} + +func (s *service) getReceivedShares(ctx context.Context) (map[string]*stattedReceivedShare, error) { + ret := map[string]*stattedReceivedShare{} + lsRes, err := s.sharesProviderClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) + if err != nil { + return nil, errors.Wrap(err, "sharesstorageprovider: error calling ListReceivedSharesRequest") + } + if lsRes.Status.Code != rpc.Code_CODE_OK { + return nil, fmt.Errorf("sharesstorageprovider: error calling ListReceivedSharesRequest") + } + appctx.GetLogger(ctx).Debug(). + Interface("ret", ret). + Interface("lsRes.Shares", lsRes.Shares). + Msg("sharesstorageprovider: Preparing statted share") + + for _, rs := range lsRes.Shares { + if rs.State != collaboration.ShareState_SHARE_STATE_ACCEPTED { + continue + } + + statRes, err := s.gateway.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + ResourceId: rs.Share.ResourceId, + }, + }) + if err != nil { + return nil, err + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + appctx.GetLogger(ctx).Debug(). + Interface("rs.Share", rs.Share). + Interface("statRes", statRes). + Msg("sharesstorageprovider: Got non-ok Stat response") + continue + } + + name := rs.GetMountPoint().GetPath() + if _, ok := ret[name]; !ok { + ret[name] = &stattedReceivedShare{ + ReceivedShare: rs, + AllReceivedShares: []*collaboration.ReceivedShare{rs}, + Stat: statRes.Info, + } + ret[name].Stat.PermissionSet = rs.Share.Permissions.Permissions + } else { + ret[name].Stat.PermissionSet = s.mergePermissions(ret[name].Stat.PermissionSet, rs.Share.Permissions.Permissions) + ret[name].AllReceivedShares = append(ret[name].AllReceivedShares, rs) + } + } + + appctx.GetLogger(ctx).Debug(). + Interface("ret", ret). + Msg("sharesstorageprovider: Returning statted share") + return ret, nil +} + +func (s *service) rejectReceivedShare(ctx context.Context, share string) error { + stattedShare, err := s.statShare(ctx, share) + if err != nil { + return err + } + + stattedShare.ReceivedShare.State = collaboration.ShareState_SHARE_STATE_REJECTED + + _, err = s.sharesProviderClient.UpdateReceivedShare(ctx, &collaboration.UpdateReceivedShareRequest{ + Share: stattedShare.ReceivedShare, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, + }) + return err +} + +func (s *service) mergePermissions(a, b *provider.ResourcePermissions) *provider.ResourcePermissions { + a.AddGrant = a.AddGrant || b.AddGrant + a.CreateContainer = a.CreateContainer || b.CreateContainer + a.Delete = a.Delete || b.Delete + a.GetPath = a.GetPath || b.GetPath + a.GetQuota = a.GetQuota || b.GetQuota + a.InitiateFileDownload = a.InitiateFileDownload || b.InitiateFileDownload + a.InitiateFileUpload = a.InitiateFileUpload || b.InitiateFileUpload + a.ListGrants = a.ListGrants || b.ListGrants + a.ListContainer = a.ListContainer || b.ListContainer + a.ListFileVersions = a.ListFileVersions || b.ListFileVersions + a.ListRecycle = a.ListRecycle || b.ListRecycle + a.Move = a.Move || b.Move + a.RemoveGrant = a.RemoveGrant || b.RemoveGrant + a.PurgeRecycle = a.PurgeRecycle || b.PurgeRecycle + a.RestoreFileVersion = a.RestoreFileVersion || b.RestoreFileVersion + a.RestoreRecycleItem = a.RestoreRecycleItem || b.RestoreRecycleItem + a.Stat = a.Stat || b.Stat + a.UpdateGrant = a.UpdateGrant || b.UpdateGrant + return a +} diff --git a/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_suite_test.go b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_suite_test.go new file mode 100644 index 0000000000..fceaad3ed3 --- /dev/null +++ b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_suite_test.go @@ -0,0 +1,31 @@ +// 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 sharesstorageprovider_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSharesstorageprovider(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sharesstorageprovider Suite") +} diff --git a/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_test.go b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_test.go new file mode 100644 index 0000000000..41eee5ae1c --- /dev/null +++ b/internal/grpc/services/sharesstorageprovider/sharesstorageprovider_test.go @@ -0,0 +1,762 @@ +// 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 sharesstorageprovider_test + +import ( + "context" + "io/ioutil" + "os" + + 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" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + provider "github.com/cs3org/reva/internal/grpc/services/sharesstorageprovider" + mocks "github.com/cs3org/reva/internal/grpc/services/sharesstorageprovider/mocks" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/rgrpc/status" + _ "github.com/cs3org/reva/pkg/share/manager/loader" + "github.com/cs3org/reva/pkg/utils" + "google.golang.org/grpc" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" +) + +var _ = Describe("Sharesstorageprovider", func() { + var ( + config = map[string]interface{}{ + "mount_path": "/shares", + "gateway_addr": "127.0.0.1:1234", + "driver": "json", + "drivers": map[string]map[string]interface{}{ + "json": {}, + }, + } + ctx = ctxpkg.ContextSetUser(context.Background(), &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: "alice", + }, + Username: "alice", + }) + + rootStatReq = &sprovider.StatRequest{ + Ref: &sprovider.Reference{ + Path: "/shares", + }, + } + rootListContainerReq = &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/shares", + }, + } + + s sprovider.ProviderAPIServer + gw *mocks.GatewayClient + sharesProviderClient *mocks.SharesProviderClient + ) + + BeforeEach(func() { + sharesProviderClient = &mocks.SharesProviderClient{} + gw = &mocks.GatewayClient{} + gw.On("ListContainer", mock.Anything, &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/share1-shareddir", + }, + }).Return( + &sprovider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + Infos: []*sprovider.ResourceInfo{ + { + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1-shareddir/share1-subdir", + Id: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "subdir", + }, + Size: 1, + }, + }, + }, nil) + + gw.On("ListContainer", mock.Anything, &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/share1-shareddir/share1-subdir", + }, + }).Return( + &sprovider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + Infos: []*sprovider.ResourceInfo{ + { + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1-shareddir/share1-subdir/share1-subdir-file", + Id: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "file", + }, + Size: 1, + }, + }, + }, nil) + + gw.On("ListContainer", mock.Anything, &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/share2-shareddir", + }, + }).Return( + &sprovider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + Infos: []*sprovider.ResourceInfo{}, + }, nil) + + gw.On("Stat", mock.Anything, mock.AnythingOfType("*providerv1beta1.StatRequest")).Return( + func(_ context.Context, req *sprovider.StatRequest, _ ...grpc.CallOption) *sprovider.StatResponse { + if req.Ref.GetPath() == "/share1-shareddir/share1-subdir" { + return &sprovider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &sprovider.ResourceInfo{ + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1-shareddir/share1-subdir", + Id: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "subdir", + }, + PermissionSet: &sprovider.ResourcePermissions{ + Stat: true, + }, + Size: 10, + }, + } + } else if req.Ref.GetPath() == "/share1-shareddir/share1-subdir/share1-subdir-file" { + return &sprovider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &sprovider.ResourceInfo{ + Type: sprovider.ResourceType_RESOURCE_TYPE_FILE, + Path: "/share1-shareddir/share1-subdir/share1-subdir-file", + Id: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "file", + }, + PermissionSet: &sprovider.ResourcePermissions{ + Stat: true, + }, + Size: 20, + }, + } + } else if req.Ref.GetPath() == "/share2-shareddir" || utils.ResourceIDEqual(req.Ref.ResourceId, &sprovider.ResourceId{ + StorageId: "share2-storageid", + OpaqueId: "shareddir", + }) { + return &sprovider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &sprovider.ResourceInfo{ + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share2-shareddir", + Id: &sprovider.ResourceId{ + StorageId: "share2-storageid", + OpaqueId: "shareddir", + }, + PermissionSet: &sprovider.ResourcePermissions{ + Stat: true, + }, + Size: 200, + }, + } + } else { + return &sprovider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &sprovider.ResourceInfo{ + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1-shareddir", + Id: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "shareddir", + }, + PermissionSet: &sprovider.ResourcePermissions{ + Stat: true, + }, + Size: 100, + }, + } + } + }, + nil) + + }) + + JustBeforeEach(func() { + p, err := provider.New("/shares", gw, sharesProviderClient) + Expect(err).ToNot(HaveOccurred()) + s = p.(sprovider.ProviderAPIServer) + Expect(s).ToNot(BeNil()) + }) + + Describe("NewDefault", func() { + It("returns a new service instance", func() { + tmpfile, err := ioutil.TempFile("", "eos-unit-test-shares-*.json") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tmpfile.Name()) + + config["drivers"] = map[string]map[string]interface{}{ + "json": { + "file": tmpfile.Name(), + }, + } + s, err := provider.NewDefault(config, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(s).ToNot(BeNil()) + }) + }) + + Describe("ListContainer", func() { + It("only considers accepted shares", func() { + sharesProviderClient.On("ListReceivedShares", mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_INVALID, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + }, + { + State: collaboration.ShareState_SHARE_STATE_REJECTED, + }, + }, + }, nil) + res, err := s.ListContainer(ctx, rootListContainerReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Infos)).To(Equal(0)) + }) + }) + + Context("with two accepted shares", func() { + BeforeEach(func() { + sharesProviderClient.On("ListReceivedShares", mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaboration.Share{ + ResourceId: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "shareddir", + }, + Permissions: &collaboration.SharePermissions{ + Permissions: &sprovider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + }, + MountPoint: &sprovider.Reference{Path: "share1-shareddir"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaboration.Share{ + ResourceId: &sprovider.ResourceId{ + StorageId: "share2-storageid", + OpaqueId: "shareddir", + }, + Permissions: &collaboration.SharePermissions{ + Permissions: &sprovider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + }, + MountPoint: &sprovider.Reference{Path: "share2-shareddir"}, + }, + }, + }, nil) + }) + + Describe("Stat", func() { + It("stats the root shares folder", func() { + res, err := s.Stat(ctx, rootStatReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Info.Type).To(Equal(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER)) + Expect(res.Info.Path).To(Equal("/shares")) + Expect(res.Info.Size).To(Equal(uint64(300))) + }) + + It("stats a shares folder", func() { + statReq := &sprovider.StatRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir", + }, + } + res, err := s.Stat(ctx, statReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Info.Type).To(Equal(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER)) + Expect(res.Info.Path).To(Equal("/shares/share1-shareddir")) + Expect(res.Info.Size).To(Equal(uint64(100))) + }) + + It("merges permissions from multiple shares", func() { + sharesProviderClient.On("ListReceivedShares", mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaboration.Share{ + ResourceId: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "shareddir", + }, + Permissions: &collaboration.SharePermissions{ + Permissions: &sprovider.ResourcePermissions{ + Stat: true, + }, + }, + }, + MountPoint: &sprovider.Reference{Path: "share1-shareddir"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaboration.Share{ + ResourceId: &sprovider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "shareddir", + }, + Permissions: &collaboration.SharePermissions{ + Permissions: &sprovider.ResourcePermissions{ + ListContainer: true, + }, + }, + }, + MountPoint: &sprovider.Reference{Path: "share2-shareddir"}, + }, + }, + }, nil) + statReq := &sprovider.StatRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir", + }, + } + res, err := s.Stat(ctx, statReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Info.Type).To(Equal(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER)) + Expect(res.Info.Path).To(Equal("/shares/share1-shareddir")) + Expect(res.Info.PermissionSet.Stat).To(BeTrue()) + Expect(res.Info.PermissionSet.ListContainer).To(BeTrue()) + }) + + It("stats a subfolder in a share", func() { + statReq := &sprovider.StatRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir", + }, + } + res, err := s.Stat(ctx, statReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Info.Type).To(Equal(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER)) + Expect(res.Info.Path).To(Equal("/shares/share1-shareddir/share1-subdir")) + Expect(res.Info.Size).To(Equal(uint64(10))) + }) + + It("stats a shared file", func() { + statReq := &sprovider.StatRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir/share1-subdir-file", + }, + } + res, err := s.Stat(ctx, statReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Info.Type).To(Equal(sprovider.ResourceType_RESOURCE_TYPE_FILE)) + Expect(res.Info.Path).To(Equal("/shares/share1-shareddir/share1-subdir/share1-subdir-file")) + Expect(res.Info.Size).To(Equal(uint64(20))) + }) + }) + + Describe("ListContainer", func() { + It("lists shares", func() { + res, err := s.ListContainer(ctx, rootListContainerReq) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(len(res.Infos)).To(Equal(2)) + + paths := []string{res.Infos[0].Path, res.Infos[1].Path} + Expect(paths).To(ConsistOf("/shares/share1-shareddir", "/shares/share2-shareddir")) + }) + + It("traverses into specific shares", func() { + req := &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir", + }, + } + res, err := s.ListContainer(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(len(res.Infos)).To(Equal(1)) + + entry := res.Infos[0] + Expect(entry.Path).To(Equal("/shares/share1-shareddir/share1-subdir")) + }) + + It("traverses into subfolders of specific shares", func() { + req := &sprovider.ListContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir", + }, + } + res, err := s.ListContainer(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(len(res.Infos)).To(Equal(1)) + + entry := res.Infos[0] + Expect(entry.Path).To(Equal("/shares/share1-shareddir/share1-subdir/share1-subdir-file")) + }) + }) + + Describe("InitiateFileDownload", func() { + It("returns not found when not found", func() { + gw.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{ + Status: status.NewNotFound(ctx, "gateway: file not found"), + }, nil) + + req := &sprovider.InitiateFileDownloadRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/does-not-exist", + }, + } + res, err := s.InitiateFileDownload(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_NOT_FOUND)) + }) + + It("initiates the download of an existing file", func() { + gw.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileDownloadProtocol{ + { + Opaque: &types.Opaque{}, + Protocol: "simple", + DownloadEndpoint: "https://localhost:9200/data", + Token: "thetoken", + }, + }, + }, nil) + req := &sprovider.InitiateFileDownloadRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir/share1-subdir-file", + }, + } + res, err := s.InitiateFileDownload(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(res.Protocols[0].Protocol).To(Equal("simple")) + Expect(res.Protocols[0].DownloadEndpoint).To(Equal("https://localhost:9200/data/thetoken")) + }) + }) + + Describe("CreateContainer", func() { + BeforeEach(func() { + gw.On("CreateContainer", mock.Anything, mock.Anything).Return(&sprovider.CreateContainerResponse{ + Status: status.NewOK(ctx), + }, nil) + }) + + It("refuses to create a top-level container which doesn't belong to a share", func() { + req := &sprovider.CreateContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/invalid-top-level-subdir", + }, + } + res, err := s.CreateContainer(ctx, req) + gw.AssertNotCalled(GinkgoT(), "CreateContainer", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) + }) + + It("creates a directory", func() { + req := &sprovider.CreateContainerRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-newsubdir", + }, + } + res, err := s.CreateContainer(ctx, req) + gw.AssertCalled(GinkgoT(), "CreateContainer", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + }) + }) + + Describe("Delete", func() { + BeforeEach(func() { + gw.On("Delete", mock.Anything, mock.Anything).Return(&sprovider.DeleteResponse{ + Status: status.NewOK(ctx), + }, nil) + }) + + It("rejects the share when deleting a share", func() { + sharesProviderClient.On("UpdateReceivedShare", mock.Anything, mock.Anything).Return(nil, nil) + req := &sprovider.DeleteRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir", + }, + } + res, err := s.Delete(ctx, req) + gw.AssertNotCalled(GinkgoT(), "Delete", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + + sharesProviderClient.AssertCalled(GinkgoT(), "UpdateReceivedShare", mock.Anything, mock.Anything) + }) + + It("deletes a file", func() { + req := &sprovider.DeleteRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir/share1-subdir-file", + }, + } + res, err := s.Delete(ctx, req) + gw.AssertCalled(GinkgoT(), "Delete", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + }) + }) + + Describe("Move", func() { + BeforeEach(func() { + gw.On("Move", mock.Anything, mock.Anything).Return(&sprovider.MoveResponse{ + Status: status.NewOK(ctx), + }, nil) + }) + + It("renames a share", func() { + sharesProviderClient.On("UpdateReceivedShare", mock.Anything, mock.Anything).Return(nil, nil) + + req := &sprovider.MoveRequest{ + Source: &sprovider.Reference{ + Path: "/shares/share1-shareddir", + }, + Destination: &sprovider.Reference{ + Path: "/shares/newname", + }, + } + res, err := s.Move(ctx, req) + gw.AssertNotCalled(GinkgoT(), "Move", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + sharesProviderClient.AssertCalled(GinkgoT(), "UpdateReceivedShare", mock.Anything, mock.Anything) + }) + + It("refuses to move a file between shares", func() { + req := &sprovider.MoveRequest{ + Source: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-file", + }, + Destination: &sprovider.Reference{ + Path: "/shares/share2-shareddir/share2-shareddir-file", + }, + } + res, err := s.Move(ctx, req) + gw.AssertNotCalled(GinkgoT(), "Move", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) + }) + + It("moves a file", func() { + req := &sprovider.MoveRequest{ + Source: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-file", + }, + Destination: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-filenew", + }, + } + res, err := s.Move(ctx, req) + gw.AssertCalled(GinkgoT(), "Move", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + }) + }) + + Describe("ListFileVersions", func() { + BeforeEach(func() { + gw.On("ListFileVersions", mock.Anything, mock.Anything).Return( + &sprovider.ListFileVersionsResponse{ + Status: status.NewOK(ctx), + Versions: []*sprovider.FileVersion{ + { + Size: 10, + Mtime: 1, + Etag: "1", + Key: "1", + }, + { + Size: 20, + Mtime: 2, + Etag: "2", + Key: "2", + }, + }, + }, nil) + }) + + It("does not try to list versions of shares or the top-level dir", func() { + req := &sprovider.ListFileVersionsRequest{ + Ref: &sprovider.Reference{ + Path: "/shares", + }, + } + res, err := s.ListFileVersions(ctx, req) + gw.AssertNotCalled(GinkgoT(), "ListFileVersions", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) + + req = &sprovider.ListFileVersionsRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/", + }, + } + res, err = s.ListFileVersions(ctx, req) + gw.AssertNotCalled(GinkgoT(), "ListFileVersions", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) + }) + + It("lists versions", func() { + req := &sprovider.ListFileVersionsRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-file", + }, + } + res, err := s.ListFileVersions(ctx, req) + gw.AssertCalled(GinkgoT(), "ListFileVersions", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(len(res.Versions)).To(Equal(2)) + version := res.Versions[0] + Expect(version.Key).To(Equal("1")) + Expect(version.Etag).To(Equal("1")) + Expect(version.Mtime).To(Equal(uint64(1))) + Expect(version.Size).To(Equal(uint64(10))) + }) + }) + + Describe("RestoreFileVersion", func() { + BeforeEach(func() { + gw.On("RestoreFileVersion", mock.Anything, mock.Anything).Return( + &sprovider.RestoreFileVersionResponse{ + Status: status.NewOK(ctx), + }, nil) + }) + + It("restores a file version", func() { + req := &sprovider.RestoreFileVersionRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-file", + }, + Key: "1", + } + res, err := s.RestoreFileVersion(ctx, req) + gw.AssertCalled(GinkgoT(), "RestoreFileVersion", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + }) + }) + + Describe("InitiateFileUpload", func() { + BeforeEach(func() { + gw.On("InitiateFileUpload", mock.Anything, mock.Anything).Return( + &gateway.InitiateFileUploadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileUploadProtocol{ + { + Opaque: &types.Opaque{}, + Protocol: "simple", + UploadEndpoint: "https://localhost:9200/data", + Token: "thetoken", + }, + }, + }, nil) + }) + + It("initiates a file upload", func() { + req := &sprovider.InitiateFileUploadRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-shareddir-file", + }, + } + res, err := s.InitiateFileUpload(ctx, req) + gw.AssertCalled(GinkgoT(), "InitiateFileUpload", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + Expect(len(res.Protocols)).To(Equal(1)) + Expect(res.Protocols[0].Protocol).To(Equal("simple")) + Expect(res.Protocols[0].UploadEndpoint).To(Equal("https://localhost:9200/data/thetoken")) + }) + }) + + Describe("SetArbitraryMetadata", func() { + BeforeEach(func() { + gw.On("SetArbitraryMetadata", mock.Anything, mock.Anything).Return(&sprovider.SetArbitraryMetadataResponse{ + Status: status.NewOK(ctx), + }, nil) + }) + + It("sets the metadata", func() { + req := &sprovider.SetArbitraryMetadataRequest{ + Ref: &sprovider.Reference{ + Path: "/shares/share1-shareddir/share1-subdir/share1-subdir-file", + }, + ArbitraryMetadata: &sprovider.ArbitraryMetadata{ + Metadata: map[string]string{ + "foo": "bar", + }, + }, + } + res, err := s.SetArbitraryMetadata(ctx, req) + gw.AssertCalled(GinkgoT(), "SetArbitraryMetadata", mock.Anything, mock.Anything) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(res.Status.Code).To(Equal(rpc.Code_CODE_OK)) + }) + }) + }) +}) diff --git a/internal/grpc/services/usershareprovider/usershareprovider.go b/internal/grpc/services/usershareprovider/usershareprovider.go index 9f5b0d1fc8..3e5b5c854a 100644 --- a/internal/grpc/services/usershareprovider/usershareprovider.go +++ b/internal/grpc/services/usershareprovider/usershareprovider.go @@ -231,7 +231,29 @@ func (s *service) GetReceivedShare(ctx context.Context, req *collaboration.GetRe } func (s *service) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) { - share, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask) // TODO(labkode): check what to update + + if req.Share == nil { + return &collaboration.UpdateReceivedShareResponse{ + Status: status.NewInvalidArg(ctx, "updating requires a received share object"), + }, nil + } + if req.Share.Share == nil { + return &collaboration.UpdateReceivedShareResponse{ + Status: status.NewInvalidArg(ctx, "share missing"), + }, nil + } + if req.Share.Share.Id == nil { + return &collaboration.UpdateReceivedShareResponse{ + Status: status.NewInvalidArg(ctx, "share id missing"), + }, nil + } + if req.Share.Share.Id.OpaqueId == "" { + return &collaboration.UpdateReceivedShareResponse{ + Status: status.NewInvalidArg(ctx, "share id empty"), + }, nil + } + + share, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask) if err != nil { return &collaboration.UpdateReceivedShareResponse{ Status: status.NewInternal(ctx, err, "error updating received share"), diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 137423013e..d0e4e8d3a1 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -20,6 +20,7 @@ package ocdav import ( "context" + "fmt" "net/http" "path" "strings" @@ -48,6 +49,7 @@ type DavHandler struct { SpacesHandler *SpacesHandler PublicFolderHandler *WebDavHandler PublicFileHandler *PublicFileHandler + SharesHandler *WebDavHandler } func (h *DavHandler) init(c *Config) error { @@ -96,6 +98,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := appctx.GetLogger(ctx) + log.Info().Str("request", fmt.Sprintf("%#v", r)).Msg("Got webdav request") // if there is no file in the request url we assume the request url is: "/remote.php/dav/files" // https://github.com/owncloud/core/blob/18475dac812064b21dabcc50f25ef3ffe55691a5/tests/acceptance/features/apiWebdavOperations/propfind.feature diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 3fa1eadb5d..3c64206642 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -99,6 +99,7 @@ type Config struct { // and received path is /docs the internal path will be: // /users///docs WebdavNamespace string `mapstructure:"webdav_namespace"` + SharesNamespace string `mapstructure:"shares_namespace"` GatewaySvc string `mapstructure:"gatewaysvc"` Timeout int64 `mapstructure:"timeout"` Insecure bool `mapstructure:"insecure"` diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go index 1fe1bf5d39..2f9ee42d91 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go @@ -19,33 +19,91 @@ package shares import ( + "context" + "fmt" "net/http" "path" + "sort" + "strconv" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/utils" "github.com/go-chi/chi/v5" "github.com/pkg/errors" "google.golang.org/protobuf/types/known/fieldmaskpb" ) +const ( + // shareID is the id of the share to update. It is present in the request URL. + shareID string = "shareid" +) + // AcceptReceivedShare handles Post Requests on /apps/files_sharing/api/v1/shares/{shareid} func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) { - shareID := chi.URLParam(r, "shareid") - h.updateReceivedShare(w, r, shareID, false) + ctx := r.Context() + shareID := chi.URLParam(r, shareID) + + client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) + return + } + + share, ocsResponse := getShareFromID(ctx, client, shareID) + if ocsResponse != nil { + response.WriteOCSResponse(w, r, *ocsResponse, nil) + return + } + + sharedResource, ocsResponse := getSharedResource(ctx, client, share) + if ocsResponse != nil { + response.WriteOCSResponse(w, r, *ocsResponse, nil) + return + } + + lrs, ocsResponse := getSharesList(ctx, client) + if ocsResponse != nil { + response.WriteOCSResponse(w, r, *ocsResponse, nil) + return + } + + // we need to sort the received shares by mount point in order to make things easier to evaluate. + var mountPoints []string + for _, s := range lrs.Shares { + if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED && !utils.ResourceIDEqual(s.Share.ResourceId, share.Share.GetResourceId()) { + // only when the share is accepted there is a mount point. + mountPoints = append(mountPoints, s.MountPoint.Path) + } + } + + sort.Strings(mountPoints) + base := path.Base(sharedResource.GetInfo().GetPath()) + mount := base + + // now we have a list of shares, we want to iterate over all of them and check for name collisions + for i, mp := range mountPoints { + if mp == mount { + mount = fmt.Sprintf("%s (%s)", base, strconv.Itoa(i+1)) + } + } + + h.updateReceivedShare(w, r, shareID, false, mount) } // RejectReceivedShare handles DELETE Requests on /apps/files_sharing/api/v1/shares/{shareid} func (h *Handler) RejectReceivedShare(w http.ResponseWriter, r *http.Request) { shareID := chi.URLParam(r, "shareid") - h.updateReceivedShare(w, r, shareID, true) + h.updateReceivedShare(w, r, shareID, true, "") } -func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, shareID string, rejectShare bool) { +func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, shareID string, rejectShare bool, mountPoint string) { ctx := r.Context() logger := appctx.GetLogger(ctx) @@ -55,16 +113,20 @@ func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, sh return } + // we need to add a path to the share shareRequest := &collaboration.UpdateReceivedShareRequest{ Share: &collaboration.ReceivedShare{ Share: &collaboration.Share{Id: &collaboration.ShareId{OpaqueId: shareID}}, + MountPoint: &provider.Reference{ + Path: mountPoint, + }, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, } if rejectShare { shareRequest.Share.State = collaboration.ShareState_SHARE_STATE_REJECTED } else { - // TODO find free mount point and pass it on with an updated field mask + shareRequest.UpdateMask.Paths = append(shareRequest.UpdateMask.Paths, "mount_point") shareRequest.Share.State = collaboration.ShareState_SHARE_STATE_ACCEPTED } @@ -109,3 +171,83 @@ func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, sh response.WriteOCSSuccess(w, r, []*conversions.ShareData{data}) } + +// getShareFromID uses a client to the gateway to fetch a share based on its ID. +func getShareFromID(ctx context.Context, client gateway.GatewayAPIClient, shareID string) (*collaboration.GetShareResponse, *response.Response) { + s, err := client.GetShare(ctx, &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: shareID, + }}, + }, + }) + + if err != nil { + e := errors.Wrap(err, fmt.Sprintf("could not get share with ID: `%s`", shareID)) + return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error()) + } + + if s.Status.Code != rpc.Code_CODE_OK { + if s.Status.Code == rpc.Code_CODE_NOT_FOUND { + e := fmt.Errorf("share not found") + return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error()) + } + + e := fmt.Errorf("invalid share: %s", s.GetStatus().GetMessage()) + return nil, arbitraryOcsResponse(response.MetaBadRequest.StatusCode, e.Error()) + } + + return s, nil +} + +// getSharedResource attempts to get a shared resource from the storage from the resource reference. +func getSharedResource(ctx context.Context, client gateway.GatewayAPIClient, share *collaboration.GetShareResponse) (*provider.StatResponse, *response.Response) { + res, err := client.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + ResourceId: share.Share.GetResourceId(), + }, + }) + if err != nil { + e := fmt.Errorf("could not get reference") + return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error()) + } + + if res.Status.Code != rpc.Code_CODE_OK { + if res.Status.Code == rpc.Code_CODE_NOT_FOUND { + e := fmt.Errorf("not found") + return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error()) + } + e := fmt.Errorf(res.GetStatus().GetMessage()) + return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error()) + } + + return res, nil +} + +// getSharedResource gets the list of all shares for the current user. +func getSharesList(ctx context.Context, client gateway.GatewayAPIClient) (*collaboration.ListReceivedSharesResponse, *response.Response) { + shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) + if err != nil { + e := errors.Wrap(err, "error getting shares list") + return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error()) + } + + if shares.Status.Code != rpc.Code_CODE_OK { + if shares.Status.Code == rpc.Code_CODE_NOT_FOUND { + e := fmt.Errorf("not found") + return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error()) + } + e := fmt.Errorf(shares.GetStatus().GetMessage()) + return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error()) + } + return shares, nil +} + +// arbitraryOcsResponse abstracts the boilerplate that is creating a response.Response struct. +func arbitraryOcsResponse(statusCode int, message string) *response.Response { + r := response.NewResponse() + r.OCS.Meta.StatusCode = statusCode + r.OCS.Meta.Message = message + return &r +} diff --git a/internal/http/services/owncloud/ocs/response/response.go b/internal/http/services/owncloud/ocs/response/response.go index f92af90875..85fc4f4cad 100644 --- a/internal/http/services/owncloud/ocs/response/response.go +++ b/internal/http/services/owncloud/ocs/response/response.go @@ -45,6 +45,17 @@ type Response struct { OCS *Payload `json:"ocs"` } +// NewResponse returns an empty response +func NewResponse() Response { + return Response{ + OCS: &Payload{ + XMLName: struct{}{}, + Meta: Meta{}, + Data: nil, + }, + } +} + // Payload combines response metadata and data type Payload struct { XMLName struct{} `json:"-" xml:"ocs"` diff --git a/pkg/cbox/share/sql/sql.go b/pkg/cbox/share/sql/sql.go index 0987aa711b..431e43f9c7 100644 --- a/pkg/cbox/share/sql/sql.go +++ b/pkg/cbox/share/sql/sql.go @@ -465,6 +465,8 @@ func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.Rece 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") } @@ -472,7 +474,7 @@ func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.Rece var query, queryAccept string params := []interface{}{rs.Share.Id.OpaqueId, conversions.FormatUserID(user.Id)} - switch rs.GetState() { + switch rs.State { case collaboration.ShareState_SHARE_STATE_REJECTED: query = "insert into oc_share_acl(id, rejected_by) values(?, ?)" case collaboration.ShareState_SHARE_STATE_ACCEPTED: diff --git a/pkg/ocm/share/manager/json/json.go b/pkg/ocm/share/manager/json/json.go index 2d91faa42c..791da87d43 100644 --- a/pkg/ocm/share/manager/json/json.go +++ b/pkg/ocm/share/manager/json/json.go @@ -644,7 +644,8 @@ func (m *mgr) UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, switch fieldMask.Paths[i] { case "state": rs.State = share.State - // TODO case "mount_point": + case "mount_point": + rs.MountPoint = share.MountPoint default: return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported") } diff --git a/pkg/share/manager/json/json.go b/pkg/share/manager/json/json.go index 55fcf2c691..27f94e2985 100644 --- a/pkg/share/manager/json/json.go +++ b/pkg/share/manager/json/json.go @@ -96,7 +96,7 @@ func loadOrCreate(file string) (*shareModel, error) { return nil, err } - m := &shareModel{State: j.State} + m := &shareModel{State: j.State, MountPoint: j.MountPoint} for _, s := range j.Shares { var decShare collaboration.Share if err = utils.UnmarshalJSONToProtoV1([]byte(s), &decShare); err != nil { @@ -108,24 +108,29 @@ func loadOrCreate(file string) (*shareModel, error) { if m.State == nil { m.State = map[string]map[string]collaboration.ShareState{} } + if m.MountPoint == nil { + m.MountPoint = map[string]map[string]*provider.Reference{} + } m.file = file return m, nil } type shareModel struct { - file string - State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState - Shares []*collaboration.Share `json:"shares"` + file string + State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState + MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint + Shares []*collaboration.Share `json:"shares"` } type jsonEncoding struct { - State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState - Shares []string `json:"shares"` + State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState + MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint + Shares []string `json:"shares"` } func (m *shareModel) Save() error { - j := &jsonEncoding{State: m.State} + j := &jsonEncoding{State: m.State, MountPoint: m.MountPoint} for _, s := range m.Shares { encShare, err := utils.MarshalProtoV1ToJSON(s) if err != nil { @@ -393,7 +398,32 @@ func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.F rss = append(rss, rs) } } - return rss, nil + + // if there is a mix-up of shares of type group and shares of type user we need to deduplicate them, since it points + // to the same resource. Leave the more explicit and hide the more explicit. In this case we hide the group shares + // and return the user share to the user. + filtered := make([]*collaboration.ReceivedShare, 0) + + for _, s := range rss { + filtered = append(filtered, s) + } + + for i := range rss { + for j := range rss { + if rss[i].Share.ResourceId.GetOpaqueId() == rss[j].Share.ResourceId.GetOpaqueId() { + if rss[i].Share.GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_GROUP && rss[j].Share.GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER { + if rss[i].State == rss[j].State { + // remove the group share from the results + filtered[i] = filtered[len(filtered)-1] + filtered[len(filtered)-1] = nil + filtered = filtered[:len(filtered)-1] + } + } + } + } + } + + return filtered, nil } // convert must be called in a lock-controlled block. @@ -408,6 +438,11 @@ func (m *mgr) convert(ctx context.Context, s *collaboration.Share) *collaboratio rs.State = state } } + if v, ok := m.model.MountPoint[user.Id.String()]; ok { + if mp, ok := v[s.Id.String()]; ok { + rs.MountPoint = mp + } + } return rs } @@ -444,22 +479,35 @@ func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaborat switch fieldMask.Paths[i] { case "state": rs.State = receivedShare.State - // TODO case "mount_point": + case "mount_point": + rs.MountPoint = receivedShare.MountPoint default: return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported") } } + // Persist state if v, ok := m.model.State[user.Id.String()]; ok { - v[rs.Share.Id.String()] = rs.GetState() + v[rs.Share.Id.String()] = rs.State m.model.State[user.Id.String()] = v } else { a := map[string]collaboration.ShareState{ - rs.Share.Id.String(): rs.GetState(), + rs.Share.Id.String(): rs.State, } m.model.State[user.Id.String()] = a } + // Persist mount point + if v, ok := m.model.MountPoint[user.Id.String()]; ok { + v[rs.Share.Id.String()] = rs.MountPoint + m.model.MountPoint[user.Id.String()] = v + } else { + a := map[string]*provider.Reference{ + rs.Share.Id.String(): rs.MountPoint, + } + m.model.MountPoint[user.Id.String()] = a + } + if err := m.model.Save(); err != nil { err = errors.Wrap(err, "error saving model") return nil, err diff --git a/pkg/share/manager/memory/memory.go b/pkg/share/manager/memory/memory.go index e292b73d72..1be0bb30ef 100644 --- a/pkg/share/manager/memory/memory.go +++ b/pkg/share/manager/memory/memory.go @@ -47,9 +47,11 @@ func init() { // New returns a new manager. func New(c map[string]interface{}) (share.Manager, error) { state := map[string]map[*collaboration.ShareId]collaboration.ShareState{} + mp := map[string]map[*collaboration.ShareId]*provider.Reference{} return &manager{ - shareState: state, - lock: &sync.Mutex{}, + shareState: state, + shareMountPoint: mp, + lock: &sync.Mutex{}, }, nil } @@ -59,6 +61,9 @@ type manager struct { // shareState contains the share state for a user. // map["alice"]["share-id"]state. shareState map[string]map[*collaboration.ShareId]collaboration.ShareState + // shareMountPoint contains the mountpoint of a share for a user. + // map["alice"]["share-id"]reference. + shareMountPoint map[string]map[*collaboration.ShareId]*provider.Reference } func (m *manager) add(ctx context.Context, s *collaboration.Share) { @@ -153,6 +158,18 @@ func (m *manager) get(ctx context.Context, ref *collaboration.ShareReference) (s return s, nil } + // or the grantee + if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, s.Grantee.GetUserId()) { + return s, nil + } else if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + // check if all user groups match this share; TODO(labkode): filter shares created by us. + for _, g := range user.Groups { + if g == s.Grantee.GetGroupId().OpaqueId { + return s, nil + } + } + } + // we return not found to not disclose information return nil, errtypes.NotFound(ref.String()) } @@ -275,6 +292,11 @@ func (m *manager) convert(ctx context.Context, s *collaboration.Share) *collabor rs.State = state } } + if v, ok := m.shareMountPoint[user.Id.String()]; ok { + if mp, ok := v[s.Id]; ok { + rs.MountPoint = mp + } + } return rs } @@ -311,21 +333,33 @@ func (m *manager) UpdateReceivedShare(ctx context.Context, receivedShare *collab switch fieldMask.Paths[i] { case "state": rs.State = receivedShare.State - // TODO case "mount_point": + case "mount_point": + rs.MountPoint = receivedShare.MountPoint default: return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported") } } + // Persist state if v, ok := m.shareState[user.Id.String()]; ok { - v[rs.Share.Id] = rs.GetState() + v[rs.Share.Id] = rs.State m.shareState[user.Id.String()] = v } else { a := map[*collaboration.ShareId]collaboration.ShareState{ - rs.Share.Id: rs.GetState(), + rs.Share.Id: rs.State, } m.shareState[user.Id.String()] = a } + // Persist mount point + if v, ok := m.shareMountPoint[user.Id.String()]; ok { + v[rs.Share.Id] = rs.MountPoint + m.shareMountPoint[user.Id.String()] = v + } else { + a := map[*collaboration.ShareId]*provider.Reference{ + rs.Share.Id: rs.MountPoint, + } + m.shareMountPoint[user.Id.String()] = a + } return rs, nil } diff --git a/pkg/share/manager/sql/conversions.go b/pkg/share/manager/sql/conversions.go index 7abf2813d7..28c28f3971 100644 --- a/pkg/share/manager/sql/conversions.go +++ b/pkg/share/manager/sql/conversions.go @@ -20,6 +20,7 @@ package sql import ( "context" + "strings" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -41,7 +42,7 @@ type DBShare struct { UIDOwner string UIDInitiator string ItemStorage string - ItemSource string + FileSource string ShareWith string Token string Expiration string @@ -52,6 +53,7 @@ type DBShare struct { FileTarget string RejectedBy string State int + Parent int } // UserConverter describes an interface for converting user ids to names and back @@ -139,7 +141,7 @@ func (m *mgr) extractGrantee(ctx context.Context, t int, g string) (*provider.Gr } grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER grantee.Id = &provider.Grantee_UserId{UserId: userid} - case 1: + case 1, 2: grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP grantee.Id = &provider.Grantee_GroupId{GroupId: extractGroupID(g)} default: @@ -232,7 +234,7 @@ func (m *mgr) convertToCS3Share(ctx context.Context, s DBShare, storageMountID s }, ResourceId: &provider.ResourceId{ StorageId: storageMountID + "!" + s.ItemStorage, - OpaqueId: s.ItemSource, + OpaqueId: s.FileSource, }, Permissions: &collaboration.SharePermissions{Permissions: permissions}, Grantee: grantee, @@ -255,7 +257,8 @@ func (m *mgr) convertToCS3ReceivedShare(ctx context.Context, s DBShare, storageM state = intToShareState(s.State) } return &collaboration.ReceivedShare{ - Share: share, - State: state, + Share: share, + State: state, + MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")}, }, nil } diff --git a/pkg/share/manager/sql/sql.go b/pkg/share/manager/sql/sql.go index 1164105637..7a4405798c 100644 --- a/pkg/share/manager/sql/sql.go +++ b/pkg/share/manager/sql/sql.go @@ -216,7 +216,7 @@ func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) er return err } owner := formatUserID(key.Owner) - query = "DELETE FROM oc_share WHERE uid_owner=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" + query = "DELETE FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" params = append(params, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid) default: return errtypes.NotFound(ref.String()) @@ -258,7 +258,7 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference return nil, err } owner := formatUserID(key.Owner) - query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" + query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid) default: return nil, errtypes.NotFound(ref.String()) @@ -277,22 +277,18 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { uid := ctxpkg.ContextMustGetUser(ctx).Username - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?)" - params := []interface{}{uid, uid} - - var ( - filterQuery string - filterParams []interface{} - err error - ) - if len(filters) == 0 { - filterQuery += "(share_type=? OR share_type=?)" - params = append(params, shareTypeUser) - params = append(params, shareTypeGroup) - } else { - filterQuery, filterParams, err = translateFilters(filters) - if err != nil { - return nil, err + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)" + var filterQuery string + params := []interface{}{uid, uid, 0, 1} + for i, f := range filters { + if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { + filterQuery += "(file_source=?)" + if i != len(filters)-1 { + filterQuery += " AND " + } + params = append(params, f.GetResourceId().OpaqueId) + } else { + return nil, fmt.Errorf("filter type is not supported") } params = append(params, filterParams...) } @@ -310,7 +306,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ( var s DBShare shares := []*collaboration.Share{} for rows.Next() { - if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { + if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { continue } share, err := m.convertToCS3Share(ctx, s, m.storageMountID) @@ -337,17 +333,27 @@ func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.F } homeConcat := "" - if m.driver == "mysql" { // mysql upsert - homeConcat = "storages.id = CONCAT('home::', ts.uid_owner)" - } else { // sqlite3 upsert - homeConcat = "storages.id = 'home::' || ts.uid_owner" + if m.driver == "mysql" { // mysql concat + homeConcat = "storages.id = CONCAT('home::', s.uid_owner)" + } else { // sqlite3 concat + homeConcat = "storages.id = 'home::' || s.uid_owner" } - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, ts.id, stime, permissions, share_type, accepted, storages.numeric_id FROM oc_share ts LEFT JOIN oc_storages storages ON " + homeConcat + " WHERE (uid_owner != ? AND uid_initiator != ?) " + userSelect := "" if len(user.Groups) > 0 { - query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))" + userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))" } else { - query += "AND (share_with=?)" - } + userSelect = "AND (share_type != 1 AND share_with=?)" + } + query := ` + WITH results AS + ( + SELECT s.*, storages.numeric_id FROM oc_share s + LEFT JOIN oc_storages storages ON ` + homeConcat + ` + WHERE (uid_owner != ? AND uid_initiator != ?) ` + userSelect + ` + ) + SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '') + AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted), + r.numeric_id, COALESCE(r.parent, -1) AS parent FROM results r LEFT JOIN results r2 ON r.id = r2.parent WHERE r.parent IS NULL;` filterQuery, filterParams, err := translateFilters(filters) if err != nil { @@ -368,7 +374,7 @@ func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.F var s DBShare shares := []*collaboration.ReceivedShare{} for rows.Next() { - if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage); err != nil { + if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil { continue } share, err := m.convertToCS3ReceivedShare(ctx, s, m.storageMountID) @@ -404,39 +410,67 @@ func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareRefe } -func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask) (*collaboration.ReceivedShare, error) { - rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: share.Share.Id}}) +func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask) (*collaboration.ReceivedShare, error) { + rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}}) if err != nil { return nil, err } + fields := []string{} + params := []interface{}{} for i := range fieldMask.Paths { switch fieldMask.Paths[i] { case "state": - rs.State = share.State - // TODO case "mount_point": + rs.State = receivedShare.State + fields = append(fields, "accepted=?") + switch rs.State { + case collaboration.ShareState_SHARE_STATE_REJECTED: + params = append(params, 2) + case collaboration.ShareState_SHARE_STATE_ACCEPTED: + params = append(params, 0) + } + case "mount_point": + fields = append(fields, "file_target=?") + rs.MountPoint = receivedShare.MountPoint + params = append(params, rs.MountPoint.Path) default: return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported") } } - var queryAccept string - switch rs.GetState() { - case collaboration.ShareState_SHARE_STATE_REJECTED: - queryAccept = "update oc_share set accepted=2 where id=?" - case collaboration.ShareState_SHARE_STATE_ACCEPTED: - queryAccept = "update oc_share set accepted=0 where id=?" + if len(fields) == 0 { + return nil, fmt.Errorf("no valid field provided in the fieldmask") } - if queryAccept != "" { - stmt, err := m.db.Prepare(queryAccept) + updateReceivedShare := func(column string) error { + query := "update oc_share set " + query += strings.Join(fields, ",") + query += fmt.Sprintf(" where %s=?", column) + queryParams := append(params, rs.Share.Id.OpaqueId) + + stmt, err := m.db.Prepare(query) if err != nil { - return nil, err + return err } - _, err = stmt.Exec(rs.Share.Id.OpaqueId) + res, err := stmt.Exec(queryParams...) if err != nil { - return nil, err + return err } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected < 1 { + return fmt.Errorf("No rows updated") + } + return nil + } + err = updateReceivedShare("parent") // Try to update the child state in case of group shares first + if err != nil { + err = updateReceivedShare("id") + } + if err != nil { + return nil, err } return rs, nil @@ -445,8 +479,8 @@ func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.Rece func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) { uid := ctxpkg.ContextMustGetUser(ctx).Username s := DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, stime, permissions, share_type FROM oc_share WHERE id=? AND (uid_owner=? or uid_initiator=?)" - if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.STime, &s.Permissions, &s.ShareType); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, stime, permissions, share_type FROM oc_share WHERE id=? AND (uid_owner=? or uid_initiator=?)" + if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(id.OpaqueId) } @@ -467,8 +501,8 @@ func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*colla if err != nil { return nil, err } - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" - if err = m.db.QueryRow(query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" + if err = m.db.QueryRow(query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(key.String()) } @@ -481,19 +515,42 @@ func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (* user := ctxpkg.ContextMustGetUser(ctx) uid := user.Username - params := []interface{}{id.OpaqueId, uid} + params := []interface{}{id.OpaqueId, id.OpaqueId, uid} for _, v := range user.Groups { params = append(params, v) } - s := DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, stime, permissions, share_type, accepted FROM oc_share ts WHERE ts.id=? " + homeConcat := "" + if m.driver == "mysql" { // mysql concat + homeConcat = "storages.id = CONCAT('home::', s.uid_owner)" + } else { // sqlite3 concat + homeConcat = "storages.id = 'home::' || s.uid_owner" + } + userSelect := "" if len(user.Groups) > 0 { - query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))" + userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))" } else { - query += "AND (share_with=?)" + userSelect = "AND (share_type != 1 AND share_with=?)" } - if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { + + query := ` + WITH results AS + ( + SELECT s.*, storages.numeric_id + FROM oc_share s + LEFT JOIN oc_storages storages ON ` + homeConcat + ` + WHERE s.id=? OR s.parent=?` + userSelect + ` + ) + SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '') + AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted), + r.numeric_id, COALESCE(r.parent, -1) AS parent + FROM results r + LEFT JOIN results r2 ON r.id = r2.parent + WHERE r.parent IS NULL; + ` + + s := DBShare{} + if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(id.OpaqueId) } @@ -516,14 +573,14 @@ func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) } s := DBShare{} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND item_source=? AND share_type=? AND share_with=? " + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? " if len(user.Groups) > 0 { query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))" } else { query += "AND (share_with=?)" } - if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { + if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(key.String()) } diff --git a/pkg/share/manager/sql/sql_test.go b/pkg/share/manager/sql/sql_test.go index 016ced3044..5837d847df 100644 --- a/pkg/share/manager/sql/sql_test.go +++ b/pkg/share/manager/sql/sql_test.go @@ -23,6 +23,7 @@ import ( "database/sql" "io/ioutil" "os" + "strconv" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -46,6 +47,7 @@ var _ = Describe("SQL manager", func() { mgr share.Manager ctx context.Context testDbFile *os.File + sqldb *sql.DB loginAs = func(user *userpb.User) { ctx = ruser.ContextSetUser(context.Background(), user) @@ -65,6 +67,16 @@ var _ = Describe("SQL manager", func() { Type: userpb.UserType_USER_TYPE_PRIMARY, }, Username: "einstein", + Groups: []string{"users"}, + } + yetAnotherUser = &userpb.User{ + Id: &userpb.UserId{ + Idp: "idp", + OpaqueId: "userid2", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "marie", + Groups: []string{"users"}, } shareRef = &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ @@ -72,6 +84,26 @@ var _ = Describe("SQL manager", func() { OpaqueId: "1", }, }} + + insertShare = func(shareType int, owner string, grantee string, parent int, source int, fileTarget string, permissions int, accepted int) (int, error) { + var parentVal interface{} + if parent >= 0 { + parentVal = parent + } + stmtString := "INSERT INTO oc_share (share_type,uid_owner,uid_initiator,item_type,item_source,file_source,parent,permissions,stime,share_with,file_target,accepted) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" + stmtValues := []interface{}{shareType, owner, owner, "folder", source, source, parentVal, permissions, 1631779730, grantee, fileTarget, accepted} + + stmt, err := sqldb.Prepare(stmtString) + if err != nil { + return -1, err + } + result, err := stmt.Exec(stmtValues...) + if err != nil { + return -1, err + } + id, err := result.LastInsertId() + return int(id), err + } ) AfterEach(func() { @@ -91,7 +123,7 @@ var _ = Describe("SQL manager", func() { err = testDbFile.Close() Expect(err).ToNot(HaveOccurred()) - sqldb, err := sql.Open("sqlite3", testDbFile.Name()) + sqldb, err = sql.Open("sqlite3", testDbFile.Name()) Expect(err).ToNot(HaveOccurred()) userConverter := &mocks.UserConverter{} @@ -182,6 +214,123 @@ var _ = Describe("SQL manager", func() { }) Describe("ListReceivedShares", func() { + Context("with a pending group share (non-autoaccept) and an accepted child share", func() { + It("only returns one share (of type group share)", func() { + loginAs(otherUser) + parentID, err := insertShare( + 1, // group share + "admin", // owner/initiator + "users", // grantee + -1, // parent + 20, // source + "/groupshared", // file_target + 31, // permissions, + 0, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + _, err = insertShare( + 2, // group child share + "admin", // owner/initiator + "einstein", // grantee + parentID, // parent + 20, // source + "/mygroupshared", // file_target + 31, // permissions, + 0, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(2)) + groupShare := shares[1] + Expect(groupShare.MountPoint.Path).To(Equal("mygroupshared")) + Expect(groupShare.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + Expect(groupShare.Share.Id.OpaqueId).To(Equal(strconv.Itoa(parentID))) + Expect(groupShare.Share.Grantee.Type).To(Equal(provider.GranteeType_GRANTEE_TYPE_GROUP)) + Expect(groupShare.Share.Grantee.GetGroupId().OpaqueId).To(Equal("users")) + }) + }) + + Context("with an accepted group share", func() { + It("lists the group share too", func() { + loginAs(otherUser) + _, err := insertShare( + 1, // group share + "admin", // owner/initiator + "users", // grantee + -1, // parent + 20, // source + "/shared", // file_target + 31, // permissions, + 0, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(2)) + groupShare := shares[1] + Expect(groupShare.MountPoint.Path).To(Equal("shared")) + Expect(groupShare.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + Expect(groupShare.Share.Grantee.Type).To(Equal(provider.GranteeType_GRANTEE_TYPE_GROUP)) + }) + + It("lists the child share information if the user changed the mountpoint", func() { + loginAs(otherUser) + parentID, err := insertShare( + 1, // group share + "admin", // owner/initiator + "users", // grantee + -1, // parent + 20, // source + "/groupshared", // file_target + 31, // permissions, + 1, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + _, err = insertShare( + 2, // group child share + "admin", // owner/initiator + "einstein", // grantee + parentID, // parent + 20, // source + "/mygroupshared", // file_target + 31, // permissions, + 0, // accepted + ) + + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(2)) + groupShare := shares[1] + Expect(groupShare.MountPoint.Path).To(Equal("mygroupshared")) + Expect(groupShare.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + Expect(groupShare.Share.Id.OpaqueId).To(Equal(strconv.Itoa(parentID))) + Expect(groupShare.Share.Grantee.Type).To(Equal(provider.GranteeType_GRANTEE_TYPE_GROUP)) + Expect(groupShare.Share.Grantee.GetGroupId().OpaqueId).To(Equal("users")) + }) + + It("does not lists group shares named like the user", func() { + loginAs(otherUser) + _, err := insertShare( + 1, // group share + "admin", // owner/initiator + "einstein", // grantee + -1, // parent + 20, // source + "/shared", // file_target + 31, // permissions, + 0, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(1)) + }) + }) + It("lists received shares", func() { loginAs(otherUser) shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) @@ -221,6 +370,12 @@ var _ = Describe("SQL manager", func() { Expect(share).ToNot(BeNil()) Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + share.State = collaboration.ShareState_SHARE_STATE_REJECTED + + share, err = mgr.UpdateReceivedShare(ctx, share, &fieldmaskpb.FieldMask{Paths: []string{"mount_point"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + share.State = collaboration.ShareState_SHARE_STATE_REJECTED share, err = mgr.UpdateReceivedShare(ctx, share, &fieldmaskpb.FieldMask{Paths: []string{"state"}}) Expect(err).ToNot(HaveOccurred()) @@ -231,6 +386,86 @@ var _ = Describe("SQL manager", func() { Expect(share).ToNot(BeNil()) Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_REJECTED)) }) + + It("updates the mount_point when the mount_point is set in the mask", func() { + loginAs(otherUser) + + share, err := mgr.GetReceivedShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + + share.MountPoint = &provider.Reference{Path: "foo"} + + share, err = mgr.UpdateReceivedShare(ctx, share, &fieldmaskpb.FieldMask{Paths: []string{"state"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(share.MountPoint.Path).To(Equal("shared")) + + share.MountPoint = &provider.Reference{Path: "foo"} + share, err = mgr.UpdateReceivedShare(ctx, share, &fieldmaskpb.FieldMask{Paths: []string{"mount_point"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(share.MountPoint.Path).To(Equal("foo")) + + share, err = mgr.GetReceivedShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.MountPoint.Path).To(Equal("foo")) + }) + + Context("with a group share", func() { + It("updates the child share with the custom information", func() { + loginAs(otherUser) + parentID, err := insertShare( + 1, // group share + "admin", // owner/initiator + "users", // grantee + -1, // parent + 20, // source + "/groupshared", // file_target + 31, // permissions, + 1, // accepted + ) + Expect(err).ToNot(HaveOccurred()) + _, err = insertShare( + 2, // group child share + "admin", // owner/initiator + "einstein", // grantee + parentID, // parent + 20, // source + "/mygroupshared", // file_target + 31, // permissions, + 0, // accepted + ) + parentRef := &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: strconv.Itoa(parentID), + }, + }} + + share, err := mgr.GetReceivedShare(ctx, parentRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + + share.MountPoint = &provider.Reference{Path: "foo"} + + By("overriding the child share information for the current user") + share, err = mgr.UpdateReceivedShare(ctx, share, &fieldmaskpb.FieldMask{Paths: []string{"mount_point"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(share.MountPoint.Path).To(Equal("foo")) + + share, err = mgr.GetReceivedShare(ctx, parentRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.MountPoint.Path).To(Equal("foo")) + + By("not overriding the parent share information") + loginAs(yetAnotherUser) + share, err = mgr.GetReceivedShare(ctx, parentRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share.MountPoint.Path).To(Equal("groupshared")) + }) + }) }) Describe("Unshare", func() { diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index ebbb7f9a30..d3672a24d9 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -48,6 +48,7 @@ import ( "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" "github.com/pkg/xattr" + "go.opentelemetry.io/otel/codes" ) // PermissionsChecker defines an interface for checking permissions on a Node @@ -313,28 +314,36 @@ func (fs *Decomposedfs) CreateDir(ctx context.Context, ref *provider.Reference) // CreateReference creates a reference as a node folder with the target stored in extended attributes // There is no difference between the /Shares folder and normal nodes because the storage is not supposed to be accessible without the storage provider. // In effect everything is a shadow namespace. -// To mimic the eos end owncloud driver we only allow references as children of the "/Shares" folder -// TODO when home support is enabled should the "/Shares" folder still be listed? +// To mimic the eos and owncloud driver we only allow references as children of the "/Shares" folder func (fs *Decomposedfs) CreateReference(ctx context.Context, p string, targetURI *url.URL) (err error) { + ctx, span := rtrace.Provider.Tracer("reva").Start(ctx, "CreateReference") + defer span.End() p = strings.Trim(p, "/") parts := strings.Split(p, "/") if len(parts) != 2 { - return errtypes.PermissionDenied("Decomposedfs: references must be a child of the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) + err := errtypes.PermissionDenied("Decomposedfs: references must be a child of the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) + span.SetStatus(codes.Error, err.Error()) + return err } if parts[0] != strings.Trim(fs.o.ShareFolder, "/") { - return errtypes.PermissionDenied("Decomposedfs: cannot create references outside the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) + err := errtypes.PermissionDenied("Decomposedfs: cannot create references outside the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) + span.SetStatus(codes.Error, err.Error()) + return err } // create Shares folder if it does not exist var n *node.Node if n, err = fs.lu.NodeFromPath(ctx, fs.o.ShareFolder, false); err != nil { - return errtypes.InternalError(err.Error()) + err := errtypes.InternalError(err.Error()) + span.SetStatus(codes.Error, err.Error()) + return err } else if !n.Exists { if err = fs.tp.CreateDir(ctx, n); err != nil { - return + span.SetStatus(codes.Error, err.Error()) + return err } } @@ -344,16 +353,24 @@ func (fs *Decomposedfs) CreateReference(ctx context.Context, p string, targetURI if n.Exists { // TODO append increasing number to mountpoint name - return errtypes.AlreadyExists(p) + err := errtypes.AlreadyExists(p) + span.SetStatus(codes.Error, err.Error()) + return err } - if err = fs.tp.CreateDir(ctx, n); err != nil { - return + if err := fs.tp.CreateDir(ctx, n); err != nil { + span.SetStatus(codes.Error, err.Error()) + return err } internal := n.InternalPath() - if err = xattr.Set(internal, xattrs.ReferenceAttr, []byte(targetURI.String())); err != nil { - return errors.Wrapf(err, "Decomposedfs: error setting the target %s on the reference file %s", targetURI.String(), internal) + if err := xattr.Set(internal, xattrs.ReferenceAttr, []byte(targetURI.String())); err != nil { + err := errors.Wrapf(err, "Decomposedfs: error setting the target %s on the reference file %s", + targetURI.String(), + internal, + ) + span.SetStatus(codes.Error, err.Error()) + return err } return nil } diff --git a/tests/oc-integration-tests/drone/gateway.toml b/tests/oc-integration-tests/drone/gateway.toml index 916ad90e01..0a687bf85d 100644 --- a/tests/oc-integration-tests/drone/gateway.toml +++ b/tests/oc-integration-tests/drone/gateway.toml @@ -72,6 +72,14 @@ home_provider = "/home" # another mount point might be "/projects/" "/public" = {"address" = "localhost:13000"} +"/home/Shares" = {"address" = "localhost:14000"} + +[grpc.services.gateway.storage_rules] +"/home" = {"address" = "localhost:12000"} +"/oc" = {"address" = "localhost:11000"} +"123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:11000"} +"/public" = {"address" = "localhost:13000"} +"/home/Shares" = {"address" = "localhost:14000"} [http] address = "0.0.0.0:19001" diff --git a/tests/oc-integration-tests/drone/storage-shares-ocis.toml b/tests/oc-integration-tests/drone/storage-shares-ocis.toml new file mode 100644 index 0000000000..97403ef3fa --- /dev/null +++ b/tests/oc-integration-tests/drone/storage-shares-ocis.toml @@ -0,0 +1,14 @@ +# This storage.toml config file will start a reva service that: +[shared] +jwt_secret = "Pive-Fumkiu4" +gatewaysvc = "localhost:19000" + +[grpc] +address = "0.0.0.0:14000" + +# This is a storage provider that grants direct access to the wrapped storage +# we have a locally running dataprovider +[grpc.services.sharesstorageprovider] +mount_path = "/home/Shares" +gateway_addr = "0.0.0.0:19000" +usershareprovidersvc = "0.0.0.0:17000" \ No newline at end of file diff --git a/tests/oc-integration-tests/local/frontend.toml b/tests/oc-integration-tests/local/frontend.toml index 7350e7b2a9..cf4bf2b69f 100644 --- a/tests/oc-integration-tests/local/frontend.toml +++ b/tests/oc-integration-tests/local/frontend.toml @@ -47,7 +47,7 @@ chunk_folder = "/var/tmp/reva/chunks" # for eos we need to rewrite the path # TODO strip the username from the path so the CS3 namespace can be mounted # at the files/ endpoint? what about migration? separate reva instance -files_namespace = "/users" +files_namespace = "/oc" # similar to the dav/files endpoint we can configure a prefix for the old webdav endpoint # we use the old webdav endpoint to present the cs3 namespace diff --git a/tests/oc-integration-tests/local/gateway.toml b/tests/oc-integration-tests/local/gateway.toml index 71289e9308..04ed6c0d8e 100644 --- a/tests/oc-integration-tests/local/gateway.toml +++ b/tests/oc-integration-tests/local/gateway.toml @@ -65,6 +65,14 @@ home_provider = "/home" # another mount point might be "/projects/" "/public" = {"address" = "localhost:13000"} +"/home/Shares" = {"address" = "localhost:14000"} + +[grpc.services.gateway.storage_rules] +"/home" = {"address" = "localhost:12000"} +"/oc" = {"address" = "localhost:11000"} +"123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:11000"} +"/public" = {"address" = "localhost:13000"} +"/home/Shares" = {"address" = "localhost:14000"} [http] address = "0.0.0.0:19001" diff --git a/tests/oc-integration-tests/local/storage-shares.toml b/tests/oc-integration-tests/local/storage-shares.toml new file mode 100644 index 0000000000..97403ef3fa --- /dev/null +++ b/tests/oc-integration-tests/local/storage-shares.toml @@ -0,0 +1,14 @@ +# This storage.toml config file will start a reva service that: +[shared] +jwt_secret = "Pive-Fumkiu4" +gatewaysvc = "localhost:19000" + +[grpc] +address = "0.0.0.0:14000" + +# This is a storage provider that grants direct access to the wrapped storage +# we have a locally running dataprovider +[grpc.services.sharesstorageprovider] +mount_path = "/home/Shares" +gateway_addr = "0.0.0.0:19000" +usershareprovidersvc = "0.0.0.0:17000" \ No newline at end of file diff --git a/tests/oc-integration-tests/local/storage-users.toml b/tests/oc-integration-tests/local/storage-users.toml index c2d4d0abf4..82784777e8 100644 --- a/tests/oc-integration-tests/local/storage-users.toml +++ b/tests/oc-integration-tests/local/storage-users.toml @@ -15,7 +15,7 @@ address = "0.0.0.0:11000" # we have a locally running dataprovider [grpc.services.storageprovider] driver = "ocis" -mount_path = "/users" +mount_path = "/oc" mount_id = "123e4567-e89b-12d3-a456-426655440000" expose_data_server = true data_server_url = "http://localhost:11001/data" From 1a6b7678bfb7355268ab32871e5476b16f098c91 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 5 Oct 2021 10:31:17 +0200 Subject: [PATCH 02/30] Refactor LDAP driver in userprovider Move some common code into helper functions. Also don't assume that the userid in reva matches the user's username. So add an additional LDAP lookup to get the username before looking up groupmembership. --- pkg/user/manager/ldap/ldap.go | 80 ++++++++++++------- .../local/ldap-users.toml | 8 +- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/pkg/user/manager/ldap/ldap.go b/pkg/user/manager/ldap/ldap.go index dbaf00b09e..e87670dfe1 100644 --- a/pkg/user/manager/ldap/ldap.go +++ b/pkg/user/manager/ldap/ldap.go @@ -149,37 +149,26 @@ func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User } defer l.Close() - // Search for the given clientID - searchRequest := ldap.NewSearchRequest( - m.c.BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - m.getUserFilter(uid), - []string{m.c.Schema.DN, m.c.Schema.UID, m.c.Schema.CN, m.c.Schema.Mail, m.c.Schema.DisplayName, m.c.Schema.UIDNumber, m.c.Schema.GIDNumber}, - nil, - ) - - sr, err := l.Search(searchRequest) + userEntry, err := m.getLDAPUserById(ctx, l, uid) if err != nil { return nil, err } - if len(sr.Entries) != 1 { - return nil, errtypes.NotFound(uid.OpaqueId) - } - - log.Debug().Interface("entries", sr.Entries).Msg("entries") + log.Debug().Interface("entry", userEntry).Msg("entries") id := &userpb.UserId{ Idp: m.c.Idp, - OpaqueId: sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.UID), + OpaqueId: userEntry.GetEqualFoldAttributeValue(m.c.Schema.UID), Type: userpb.UserType_USER_TYPE_PRIMARY, } - groups, err := m.GetUserGroups(ctx, id) + + groups, err := m.getLDAPUserGroups(ctx, l, userEntry) if err != nil { return nil, err } + gidNumber := m.c.Nobody - gidValue := sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.GIDNumber) + gidValue := userEntry.GetEqualFoldAttributeValue(m.c.Schema.GIDNumber) if gidValue != "" { gidNumber, err = strconv.ParseInt(gidValue, 10, 64) if err != nil { @@ -187,7 +176,7 @@ func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User } } uidNumber := m.c.Nobody - uidValue := sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.UIDNumber) + uidValue := userEntry.GetEqualFoldAttributeValue(m.c.Schema.UIDNumber) if uidValue != "" { uidNumber, err = strconv.ParseInt(uidValue, 10, 64) if err != nil { @@ -196,10 +185,10 @@ func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User } u := &userpb.User{ Id: id, - Username: sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.CN), + Username: userEntry.GetEqualFoldAttributeValue(m.c.Schema.CN), Groups: groups, - Mail: sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.Mail), - DisplayName: sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.DisplayName), + Mail: userEntry.GetEqualFoldAttributeValue(m.c.Schema.Mail), + DisplayName: userEntry.GetEqualFoldAttributeValue(m.c.Schema.DisplayName), GidNumber: gidNumber, UidNumber: uidNumber, } @@ -256,7 +245,7 @@ func (m *manager) GetUserByClaim(ctx context.Context, claim, value string) (*use OpaqueId: sr.Entries[0].GetEqualFoldAttributeValue(m.c.Schema.UID), Type: userpb.UserType_USER_TYPE_PRIMARY, } - groups, err := m.GetUserGroups(ctx, id) + groups, err := m.getLDAPUserGroups(ctx, l, sr.Entries[0]) if err != nil { return nil, err } @@ -319,7 +308,7 @@ func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, OpaqueId: entry.GetEqualFoldAttributeValue(m.c.Schema.UID), Type: userpb.UserType_USER_TYPE_PRIMARY, } - groups, err := m.GetUserGroups(ctx, id) + groups, err := m.getLDAPUserGroups(ctx, l, entry) if err != nil { return nil, err } @@ -361,16 +350,50 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri } defer l.Close() - // Search for the given clientID + userEntry, err := m.getLDAPUserById(ctx, l, uid) + return m.getLDAPUserGroups(ctx, l, userEntry) +} + +func (m *manager) getLDAPUserById(ctx context.Context, conn *ldap.Conn, uid *userpb.UserId) (*ldap.Entry, error) { + log := appctx.GetLogger(ctx) + // Search for the given clientID, use a sizeLimit of 1 to be able + // to error out early when the userid is not unique + searchRequest := ldap.NewSearchRequest( + m.c.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + m.getUserFilter(uid), + []string{m.c.Schema.DN, m.c.Schema.UID, m.c.Schema.CN, m.c.Schema.Mail, m.c.Schema.DisplayName, m.c.Schema.UIDNumber, m.c.Schema.GIDNumber}, + nil, + ) + + sr, err := conn.Search(searchRequest) + if err != nil { + if lerr, ok := err.(*ldap.Error); ok { + if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { + log.Error().Err(lerr).Msg(fmt.Sprintf("userid '%s' is not unique", uid)) + } + } + return nil, errtypes.NotFound(uid.OpaqueId) + } + + if len(sr.Entries) == 0 { + return nil, errtypes.NotFound(uid.OpaqueId) + } + return sr.Entries[0], nil + +} + +func (m *manager) getLDAPUserGroups(ctx context.Context, conn *ldap.Conn, userEntry *ldap.Entry) ([]string, error) { + username := userEntry.GetEqualFoldAttributeValue(m.c.Schema.CN) searchRequest := ldap.NewSearchRequest( m.c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - m.getGroupFilter(uid), + m.getGroupFilter(username), []string{m.c.Schema.CN}, // TODO use DN to look up group id nil, ) - sr, err := l.Search(searchRequest) + sr, err := conn.Search(searchRequest) if err != nil { return []string{}, err } @@ -383,7 +406,6 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri // FIXME 2. ook up the id for each group groups = append(groups, entry.GetEqualFoldAttributeValue(m.c.Schema.CN)) } - return groups, nil } @@ -405,7 +427,7 @@ func (m *manager) getFindFilter(query string) string { return strings.ReplaceAll(m.c.FindFilter, "{{query}}", ldap.EscapeFilter(query)) } -func (m *manager) getGroupFilter(uid *userpb.UserId) string { +func (m *manager) getGroupFilter(uid interface{}) string { b := bytes.Buffer{} if err := m.groupfilter.Execute(&b, uid); err != nil { err := errors.Wrap(err, fmt.Sprintf("error executing group template: userid:%+v", uid)) diff --git a/tests/oc-integration-tests/local/ldap-users.toml b/tests/oc-integration-tests/local/ldap-users.toml index d068a0eaca..2f9602e838 100644 --- a/tests/oc-integration-tests/local/ldap-users.toml +++ b/tests/oc-integration-tests/local/ldap-users.toml @@ -20,7 +20,7 @@ bind_username="cn=admin,dc=owncloud,dc=com" bind_password="admin" idp="http://localhost:18000" [grpc.services.authprovider.auth_managers.ldap.schema] -uid="uid" +uid="entryuuid" displayName="displayName" dn="dn" cn="cn" @@ -33,16 +33,16 @@ hostname="localhost" port=636 insecure=true base_dn="dc=owncloud,dc=com" -userfilter="(&(objectclass=posixAccount)(|(uid={{.OpaqueId}})(cn={{.OpaqueId}})))" +userfilter="(&(objectclass=posixAccount)(|(entryuuid={{.OpaqueId}})(cn={{.OpaqueId}})))" findfilter="(&(objectclass=posixAccount)(|(cn={{query}}*)(displayname={{query}}*)(mail={{query}}*)))" attributefilter="(&(objectclass=posixAccount)({{attr}}={{value}}))" -groupfilter="(&(objectclass=posixGroup)(cn=*)(memberuid={{.OpaqueId}}))" +groupfilter="(&(objectclass=posixGroup)(cn=*)(memberuid={{.}}))" bind_username="cn=admin,dc=owncloud,dc=com" bind_password="admin" idp="http://localhost:18000" [grpc.services.userprovider.drivers.ldap.schema] -uid="uid" +uid="entryuuid" displayName="displayName" dn="dn" cn="cn" From 2a8affc81c66c0a74211455cec819c4375eedf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Thu, 7 Oct 2021 08:38:20 +0200 Subject: [PATCH 03/30] Fix the templating when resolving dav URLs --- .../http/services/owncloud/ocdav/ocdav.go | 25 +++++++++++++++---- .../http/services/owncloud/ocdav/webdav.go | 14 ++++++++++- .../oc-integration-tests/drone/frontend.toml | 2 +- .../oc-integration-tests/local/frontend.toml | 2 +- tests/oc-integration-tests/local/gateway.toml | 2 +- .../local/storage-users.toml | 2 +- 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 3c64206642..3719b10e0d 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -32,6 +32,7 @@ import ( 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" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" @@ -236,7 +237,7 @@ func (s *svc) getClient() (gateway.GatewayAPIClient, error) { return pool.GetGatewayServiceClient(s.c.GatewaySvc) } -func applyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) string { +func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) (string, string, error) { // If useLoggedInUserNS is false, that implies that the request is coming from // the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner // is not the same as the logged in user. In that case, we'll treat fileOwner @@ -244,12 +245,26 @@ func applyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, request // namespace template. u, ok := ctxpkg.ContextGetUser(ctx) if !ok || !useLoggedInUserNS { - requestUserID, _ := router.ShiftPath(requestPath) - u = &userpb.User{ - Username: requestUserID, + var requestUserID string + requestUserID, requestPath = router.ShiftPath(requestPath) + + gatewayClient, err := s.getClient() + if err != nil { + return "", "", err + } + userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ + UserId: &userpb.UserId{OpaqueId: requestUserID}, + }) + if err != nil { + return "", "", err } + if userRes.Status.Code != rpc.Code_CODE_OK { + return "", "", errors.New(userRes.Status.Message) + } + + u = userRes.User } - return templates.WithUser(u, ns) + return templates.WithUser(u, ns), requestPath, nil } func wrapResourceID(r *provider.ResourceId) string { diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index 246a7a8e01..2286e0b60e 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -19,8 +19,11 @@ package ocdav import ( + "fmt" "net/http" "path" + + "github.com/cs3org/reva/pkg/appctx" ) // Common Webdav methods. @@ -91,7 +94,16 @@ func (h *WebDavHandler) init(ns string, useLoggedInUserNS bool) error { // Handler handles requests func (h *WebDavHandler) Handler(s *svc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ns := applyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path) + ns, newPath, err := s.ApplyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path) + if err != nil { + w.WriteHeader(http.StatusNotFound) + b, err := Marshal(exception{ + code: SabredavNotFound, + message: fmt.Sprintf("could not get storage for %s", r.RemoteAddr), + }) + HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err) + } + r.URL.Path = newPath switch r.Method { case MethodPropfind: s.handlePathPropfind(w, r, ns) diff --git a/tests/oc-integration-tests/drone/frontend.toml b/tests/oc-integration-tests/drone/frontend.toml index 13fbba77dc..b0424003d9 100644 --- a/tests/oc-integration-tests/drone/frontend.toml +++ b/tests/oc-integration-tests/drone/frontend.toml @@ -47,7 +47,7 @@ chunk_folder = "/drone/src/tmp/reva/chunks" # for eos we need to rewrite the path # TODO strip the username from the path so the CS3 namespace can be mounted # at the files/ endpoint? what about migration? separate reva instance -files_namespace = "/users" +files_namespace = "/users/{{.Id.OpaqueId}}" # similar to the dav/files endpoint we can configure a prefix for the old webdav endpoint # we use the old webdav endpoint to present the cs3 namespace diff --git a/tests/oc-integration-tests/local/frontend.toml b/tests/oc-integration-tests/local/frontend.toml index cf4bf2b69f..ef2dc35507 100644 --- a/tests/oc-integration-tests/local/frontend.toml +++ b/tests/oc-integration-tests/local/frontend.toml @@ -47,7 +47,7 @@ chunk_folder = "/var/tmp/reva/chunks" # for eos we need to rewrite the path # TODO strip the username from the path so the CS3 namespace can be mounted # at the files/ endpoint? what about migration? separate reva instance -files_namespace = "/oc" +files_namespace = "/users/{{.Id.OpaqueId}}" # similar to the dav/files endpoint we can configure a prefix for the old webdav endpoint # we use the old webdav endpoint to present the cs3 namespace diff --git a/tests/oc-integration-tests/local/gateway.toml b/tests/oc-integration-tests/local/gateway.toml index 04ed6c0d8e..977ec1bee9 100644 --- a/tests/oc-integration-tests/local/gateway.toml +++ b/tests/oc-integration-tests/local/gateway.toml @@ -69,7 +69,7 @@ home_provider = "/home" [grpc.services.gateway.storage_rules] "/home" = {"address" = "localhost:12000"} -"/oc" = {"address" = "localhost:11000"} +"/users" = {"address" = "localhost:11000"} "123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:11000"} "/public" = {"address" = "localhost:13000"} "/home/Shares" = {"address" = "localhost:14000"} diff --git a/tests/oc-integration-tests/local/storage-users.toml b/tests/oc-integration-tests/local/storage-users.toml index 82784777e8..c2d4d0abf4 100644 --- a/tests/oc-integration-tests/local/storage-users.toml +++ b/tests/oc-integration-tests/local/storage-users.toml @@ -15,7 +15,7 @@ address = "0.0.0.0:11000" # we have a locally running dataprovider [grpc.services.storageprovider] driver = "ocis" -mount_path = "/oc" +mount_path = "/users" mount_id = "123e4567-e89b-12d3-a456-426655440000" expose_data_server = true data_server_url = "http://localhost:11001/data" From 774ce44e5368c9f3a4191229baa5b78fe0ec9b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Thu, 7 Oct 2021 09:41:24 +0200 Subject: [PATCH 04/30] Fix listing shares using the SQL manager --- pkg/share/manager/sql/sql.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/share/manager/sql/sql.go b/pkg/share/manager/sql/sql.go index 7a4405798c..15a08e3375 100644 --- a/pkg/share/manager/sql/sql.go +++ b/pkg/share/manager/sql/sql.go @@ -277,18 +277,22 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { uid := ctxpkg.ContextMustGetUser(ctx).Username - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)" - var filterQuery string - params := []interface{}{uid, uid, 0, 1} - for i, f := range filters { - if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { - filterQuery += "(file_source=?)" - if i != len(filters)-1 { - filterQuery += " AND " - } - params = append(params, f.GetResourceId().OpaqueId) - } else { - return nil, fmt.Errorf("filter type is not supported") + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?)" + params := []interface{}{uid, uid} + + var ( + filterQuery string + filterParams []interface{} + err error + ) + if len(filters) == 0 { + filterQuery += "(share_type=? OR share_type=?)" + params = append(params, shareTypeUser) + params = append(params, shareTypeGroup) + } else { + filterQuery, filterParams, err = translateFilters(filters) + if err != nil { + return nil, err } params = append(params, filterParams...) } From 277332c2a3e40179b07127a6b4ac9f1a3c80faad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Thu, 7 Oct 2021 10:07:51 +0200 Subject: [PATCH 05/30] Fix gateway drone config --- tests/oc-integration-tests/drone/gateway.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oc-integration-tests/drone/gateway.toml b/tests/oc-integration-tests/drone/gateway.toml index 0a687bf85d..c339b730bf 100644 --- a/tests/oc-integration-tests/drone/gateway.toml +++ b/tests/oc-integration-tests/drone/gateway.toml @@ -76,7 +76,7 @@ home_provider = "/home" [grpc.services.gateway.storage_rules] "/home" = {"address" = "localhost:12000"} -"/oc" = {"address" = "localhost:11000"} +"/users" = {"address" = "localhost:11000"} "123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:11000"} "/public" = {"address" = "localhost:13000"} "/home/Shares" = {"address" = "localhost:14000"} From 98f7aec32f234294fb659c6c7057bfbb96d31b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 8 Oct 2021 10:37:58 +0200 Subject: [PATCH 06/30] Adapt ldap config in drone --- tests/oc-integration-tests/drone/ldap-users.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/oc-integration-tests/drone/ldap-users.toml b/tests/oc-integration-tests/drone/ldap-users.toml index 7bb96e0c9a..c687c4ac45 100644 --- a/tests/oc-integration-tests/drone/ldap-users.toml +++ b/tests/oc-integration-tests/drone/ldap-users.toml @@ -20,7 +20,7 @@ bind_username="cn=admin,dc=owncloud,dc=com" bind_password="admin" idp="http://localhost:18000" [grpc.services.authprovider.auth_managers.ldap.schema] -uid="uid" +uid="entryuuid" displayName="displayName" dn="dn" cn="cn" @@ -33,16 +33,16 @@ hostname="ldap" port=636 insecure=true base_dn="dc=owncloud,dc=com" -userfilter="(&(objectclass=posixAccount)(|(uid={{.OpaqueId}})(cn={{.OpaqueId}})))" +userfilter="(&(objectclass=posixAccount)(|(entryuuid={{.OpaqueId}})(cn={{.OpaqueId}})))" findfilter="(&(objectclass=posixAccount)(|(cn={{query}}*)(displayname={{query}}*)(mail={{query}}*)))" attributefilter="(&(objectclass=posixAccount)({{attr}}={{value}}))" -groupfilter="(&(objectclass=posixGroup)(cn=*)(memberuid={{.OpaqueId}}))" +groupfilter="(&(objectclass=posixGroup)(cn=*)(memberuid={{.}}))" bind_username="cn=admin,dc=owncloud,dc=com" bind_password="admin" idp="http://localhost:18000" [grpc.services.userprovider.drivers.ldap.schema] -uid="uid" +uid="entryuuid" displayName="displayName" dn="dn" cn="cn" From 550c9376003e5efddc04c0295720afb461729173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 8 Oct 2021 10:50:21 +0200 Subject: [PATCH 07/30] Add changelog --- changelog/unreleased/sharestorageprovider.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/unreleased/sharestorageprovider.md diff --git a/changelog/unreleased/sharestorageprovider.md b/changelog/unreleased/sharestorageprovider.md new file mode 100644 index 0000000000..14675d3178 --- /dev/null +++ b/changelog/unreleased/sharestorageprovider.md @@ -0,0 +1,5 @@ +Change: Add a sharestorageprovider + +This PR adds a ShareStorageProvider which enables us to get rid of a lot of special casing in other parts of the code. It also fixes several issues regarding shares and group shares. + +https://github.com/cs3org/reva/pull/2023 \ No newline at end of file From 245281e6192e0b87349425d408f83dd63a28a110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 8 Oct 2021 11:11:34 +0200 Subject: [PATCH 08/30] Remove now passing tests from the expected failure file --- .../expected-failures-on-OCIS-storage.md | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index e526005b0e..9a09afbc7b 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -18,14 +18,6 @@ These tests are about overwriting files or folders in the `Shares` folder of a u - [apiWebdavProperties1/copyFile.feature:227](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L227) - [apiWebdavProperties1/copyFile.feature:244](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L244) - [apiWebdavProperties1/copyFile.feature:245](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L245) -- [apiWebdavProperties1/copyFile.feature:267](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L267) -- [apiWebdavProperties1/copyFile.feature:268](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L268) -- [apiWebdavProperties1/copyFile.feature:292](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L292) -- [apiWebdavProperties1/copyFile.feature:293](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L293) -- [apiWebdavProperties1/copyFile.feature:316](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L316) -- [apiWebdavProperties1/copyFile.feature:317](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L317) -- [apiWebdavProperties1/copyFile.feature:340](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L340) -- [apiWebdavProperties1/copyFile.feature:341](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L341) #### [Custom dav properties with namespaces are rendered incorrectly](https://github.com/owncloud/ocis/issues/2140) _ocdav: double check the webdav property parsing when custom namespaces are used_ @@ -516,7 +508,6 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt #### [path property in pending shares gives only filename](https://github.com/owncloud/ocis/issues/2156) - [apiShareReshareToShares2/reShareSubfolder.feature:180](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares2/reShareSubfolder.feature#L180) - [apiShareReshareToShares2/reShareSubfolder.feature:181](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares2/reShareSubfolder.feature#L181) -- [apiShareManagementBasicToShares/deleteShareFromShares.feature:59](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/deleteShareFromShares.feature#L59) - [apiShareManagementBasicToShares/createShareToSharesFolder.feature:735](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L735) - [apiShareManagementBasicToShares/createShareToSharesFolder.feature:736](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L736) - [apiShareManagementBasicToShares/createShareToSharesFolder.feature:754](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L754) @@ -544,12 +535,6 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt - [apiTrashbin/trashbinSharingToShares.feature:154](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinSharingToShares.feature#L154) - [apiTrashbin/trashbinSharingToShares.feature:155](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinSharingToShares.feature#L155) -#### [Folder overwrite on shared files doesn't works correctly on copying file](https://github.com/owncloud/ocis/issues/2183) -- [apiWebdavProperties1/copyFile.feature:409](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L409) -- [apiWebdavProperties1/copyFile.feature:410](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L410) -- [apiWebdavProperties1/copyFile.feature:491](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L491) -- [apiWebdavProperties1/copyFile.feature:492](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L492) - #### [changing user quota gives ocs status 103 / cannot set user quota using the ocs endpoint](https://github.com/owncloud/product/issues/247) _getting and setting quota_ _requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ @@ -660,8 +645,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p #### [Expiration date for shares is not implemented](https://github.com/owncloud/ocis/issues/1250) #### Expiration date of user shares -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:29](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L29) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:30](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L30) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:58](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L58) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:59](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L59) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:86](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L86) @@ -670,8 +653,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:114](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L114) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:140](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L140) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:141](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L141) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L162) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L163) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:303](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L303) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:304](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L304) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:325](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L325) @@ -684,10 +665,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:389](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L389) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:406](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L406) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:407](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L407) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:566](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L566) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:567](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L567) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:584](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L584) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:585](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L585) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:606](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L606) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:607](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L607) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:631](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L631) @@ -729,38 +706,20 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareReshareToShares3/reShareWithExpiryDate.feature:37](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L37) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:92](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L92) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:93](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L93) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:94](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L94) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:95](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L95) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:124](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L124) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:125](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L125) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:126](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L126) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:127](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L127) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:153](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L153) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:154](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L154) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:155](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L155) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:156](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L156) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:186](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L186) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:187](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L187) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:215](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L215) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:216](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L216) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:217](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L217) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:218](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L218) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:273](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L273) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:274](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L274) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:275](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L275) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:276](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L276) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:305](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L305) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:306](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L306) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:307](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L307) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:308](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L308) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:338](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L338) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:339](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L339) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:340](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L340) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:341](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L341) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:368](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L368) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:369](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L369) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:370](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L370) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:371](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L371) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:403](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L403) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:404](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L404) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:405](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L405) @@ -973,12 +932,6 @@ And other missing implementation of favorites - [apiFavorites/favorites.feature:149](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L149) - [apiFavorites/favorites.feature:176](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L176) - [apiFavorites/favorites.feature:177](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L177) -- [apiFavorites/favoritesSharingToShares.feature:21](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L21) -- [apiFavorites/favoritesSharingToShares.feature:22](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L22) -- [apiFavorites/favoritesSharingToShares.feature:35](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L35) -- [apiFavorites/favoritesSharingToShares.feature:36](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L36) -- [apiFavorites/favoritesSharingToShares.feature:48](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L48) -- [apiFavorites/favoritesSharingToShares.feature:49](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L49) - [apiFavorites/favoritesSharingToShares.feature:62](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L62) - [apiFavorites/favoritesSharingToShares.feature:63](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L63) @@ -1320,10 +1273,6 @@ _ocs: api compatibility, return correct status code_ - [apiWebdavProperties1/copyFile.feature:363](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L363) - [apiWebdavProperties1/copyFile.feature:383](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L483) - [apiWebdavProperties1/copyFile.feature:384](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L484) -- [apiWebdavProperties1/copyFile.feature:437](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L437) -- [apiWebdavProperties1/copyFile.feature:438](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L438) -- [apiWebdavProperties1/copyFile.feature:464](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L464) -- [apiWebdavProperties1/copyFile.feature:465](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L465) ### [not possible to overwrite a received shared file](https://github.com/owncloud/ocis/issues/2267) - [apiShareOperationsToShares1/changingFilesShare.feature:114](https://github.com/owncloud/web/blob/master/tests/acceptance/features/apiShareOperationsToShares1/changingFilesShare.feature#L114) From e02af92381698e016aa9601b3eed7a9f8389dad2 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Thu, 7 Oct 2021 16:33:56 +0200 Subject: [PATCH 09/30] ocdav: Use GetUserByClaim to lookup user by name GetUser() is for getting users by ID only. Also try to handle the "User does not exist" case with a more helpful error message. --- .../http/services/owncloud/ocdav/ocdav.go | 31 ++++++++++++++++--- .../http/services/owncloud/ocdav/webdav.go | 2 +- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 3719b10e0d..10e07b67bd 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -245,25 +245,46 @@ func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool // namespace template. u, ok := ctxpkg.ContextGetUser(ctx) if !ok || !useLoggedInUserNS { - var requestUserID string - requestUserID, requestPath = router.ShiftPath(requestPath) + var requestUsernameOrID string + requestUsernameOrID, requestPath = router.ShiftPath(requestPath) gatewayClient, err := s.getClient() if err != nil { return "", "", err } + + // Check if this is a Userid userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ - UserId: &userpb.UserId{OpaqueId: requestUserID}, + UserId: &userpb.UserId{OpaqueId: requestUsernameOrID}, }) if err != nil { return "", "", err } - if userRes.Status.Code != rpc.Code_CODE_OK { - return "", "", errors.New(userRes.Status.Message) + + // If it's not a userid try if it is a user name + if userRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + res, err := gatewayClient.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ + Claim: "username", + Value: requestUsernameOrID, + }) + if err != nil { + return "", "", err + } + userRes.Status = res.Status + userRes.User = res.User + } + + // If still didn't find a user, fallback + if userRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + userRes.User = &userpb.User{ + Username: requestUsernameOrID, + Id: &userpb.UserId{OpaqueId: requestUsernameOrID}, + } } u = userRes.User } + return templates.WithUser(u, ns), requestPath, nil } diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index 2286e0b60e..abd984b9ff 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -99,7 +99,7 @@ func (h *WebDavHandler) Handler(s *svc) http.Handler { w.WriteHeader(http.StatusNotFound) b, err := Marshal(exception{ code: SabredavNotFound, - message: fmt.Sprintf("could not get storage for %s", r.RemoteAddr), + message: fmt.Sprintf("could not get storage for %s", r.URL.Path), }) HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err) } From 683f68f145c52056497cc3ab523b56525ceccd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Thu, 14 Oct 2021 12:00:13 +0200 Subject: [PATCH 10/30] Accept all shares to the same resource when accepting one of them --- .../sharing/shares/mocks/GatewayClient.go | 264 ++++++++++++++++ .../handlers/apps/sharing/shares/pending.go | 35 +- .../apps/sharing/shares/pending_test.go | 298 ++++++++++++++++++ .../apps/sharing/shares/private_test.go | 72 +++++ .../handlers/apps/sharing/shares/shares.go | 46 ++- .../apps/sharing/shares/shares_suite_test.go | 13 + .../apps/sharing/shares/shares_test.go | 158 +++++++--- internal/http/services/owncloud/ocs/ocs.go | 2 +- 8 files changed, 816 insertions(+), 72 deletions(-) create mode 100644 internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go create mode 100644 internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go create mode 100644 internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/private_test.go create mode 100644 internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go new file mode 100644 index 0000000000..fd04a523bc --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -0,0 +1,264 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + + groupv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" +) + +// GatewayClient is an autogenerated mock type for the GatewayClient type +type GatewayClient struct { + mock.Mock +} + +// CreateShare provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) CreateShare(ctx context.Context, in *collaborationv1beta1.CreateShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.CreateShareResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.CreateShareResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.CreateShareRequest, ...grpc.CallOption) *collaborationv1beta1.CreateShareResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.CreateShareResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.CreateShareRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGroup provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) GetGroup(ctx context.Context, in *groupv1beta1.GetGroupRequest, opts ...grpc.CallOption) (*groupv1beta1.GetGroupResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *groupv1beta1.GetGroupResponse + if rf, ok := ret.Get(0).(func(context.Context, *groupv1beta1.GetGroupRequest, ...grpc.CallOption) *groupv1beta1.GetGroupResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*groupv1beta1.GetGroupResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *groupv1beta1.GetGroupRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetShare provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) GetShare(ctx context.Context, in *collaborationv1beta1.GetShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetShareResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.GetShareResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.GetShareRequest, ...grpc.CallOption) *collaborationv1beta1.GetShareResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.GetShareResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.GetShareRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUser provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) GetUser(ctx context.Context, in *userv1beta1.GetUserRequest, opts ...grpc.CallOption) (*userv1beta1.GetUserResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *userv1beta1.GetUserResponse + if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.GetUserRequest, ...grpc.CallOption) *userv1beta1.GetUserResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*userv1beta1.GetUserResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.GetUserRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListContainer provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) ListContainer(ctx context.Context, in *providerv1beta1.ListContainerRequest, opts ...grpc.CallOption) (*providerv1beta1.ListContainerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.ListContainerResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) *providerv1beta1.ListContainerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.ListContainerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListReceivedShares provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) ListReceivedShares(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.ListReceivedSharesResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ListReceivedSharesRequest, ...grpc.CallOption) *collaborationv1beta1.ListReceivedSharesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.ListReceivedSharesResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.ListReceivedSharesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Stat provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Stat(ctx context.Context, in *providerv1beta1.StatRequest, opts ...grpc.CallOption) (*providerv1beta1.StatResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *providerv1beta1.StatResponse + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) *providerv1beta1.StatResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providerv1beta1.StatResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateReceivedShare provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) UpdateReceivedShare(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.UpdateReceivedShareResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.UpdateReceivedShareRequest, ...grpc.CallOption) *collaborationv1beta1.UpdateReceivedShareResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.UpdateReceivedShareResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.UpdateReceivedShareRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go index 2f9ee42d91..0697018735 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go @@ -26,14 +26,12 @@ import ( "sort" "strconv" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/utils" "github.com/go-chi/chi/v5" "github.com/pkg/errors" @@ -49,8 +47,7 @@ const ( func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) { ctx := r.Context() shareID := chi.URLParam(r, shareID) - - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + client, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return @@ -75,17 +72,25 @@ func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) { } // we need to sort the received shares by mount point in order to make things easier to evaluate. + base := path.Base(sharedResource.GetInfo().GetPath()) + mount := base var mountPoints []string + sharesToAccept := map[string]bool{shareID: true} for _, s := range lrs.Shares { - if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED && !utils.ResourceIDEqual(s.Share.ResourceId, share.Share.GetResourceId()) { - // only when the share is accepted there is a mount point. - mountPoints = append(mountPoints, s.MountPoint.Path) + if utils.ResourceIDEqual(s.Share.ResourceId, share.Share.GetResourceId()) { + if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED { + mount = s.MountPoint.Path + } else { + sharesToAccept[s.Share.Id.OpaqueId] = true + } + } else { + if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED { + mountPoints = append(mountPoints, s.MountPoint.Path) + } } } sort.Strings(mountPoints) - base := path.Base(sharedResource.GetInfo().GetPath()) - mount := base // now we have a list of shares, we want to iterate over all of them and check for name collisions for i, mp := range mountPoints { @@ -94,7 +99,9 @@ func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) { } } - h.updateReceivedShare(w, r, shareID, false, mount) + for id, _ := range sharesToAccept { + h.updateReceivedShare(w, r, id, false, mount) + } } // RejectReceivedShare handles DELETE Requests on /apps/files_sharing/api/v1/shares/{shareid} @@ -107,7 +114,7 @@ func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, sh ctx := r.Context() logger := appctx.GetLogger(ctx) - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + client, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return @@ -173,7 +180,7 @@ func (h *Handler) updateReceivedShare(w http.ResponseWriter, r *http.Request, sh } // getShareFromID uses a client to the gateway to fetch a share based on its ID. -func getShareFromID(ctx context.Context, client gateway.GatewayAPIClient, shareID string) (*collaboration.GetShareResponse, *response.Response) { +func getShareFromID(ctx context.Context, client GatewayClient, shareID string) (*collaboration.GetShareResponse, *response.Response) { s, err := client.GetShare(ctx, &collaboration.GetShareRequest{ Ref: &collaboration.ShareReference{ Spec: &collaboration.ShareReference_Id{ @@ -202,7 +209,7 @@ func getShareFromID(ctx context.Context, client gateway.GatewayAPIClient, shareI } // getSharedResource attempts to get a shared resource from the storage from the resource reference. -func getSharedResource(ctx context.Context, client gateway.GatewayAPIClient, share *collaboration.GetShareResponse) (*provider.StatResponse, *response.Response) { +func getSharedResource(ctx context.Context, client GatewayClient, share *collaboration.GetShareResponse) (*provider.StatResponse, *response.Response) { res, err := client.Stat(ctx, &provider.StatRequest{ Ref: &provider.Reference{ ResourceId: share.Share.GetResourceId(), @@ -226,7 +233,7 @@ func getSharedResource(ctx context.Context, client gateway.GatewayAPIClient, sha } // getSharedResource gets the list of all shares for the current user. -func getSharesList(ctx context.Context, client gateway.GatewayAPIClient) (*collaboration.ListReceivedSharesResponse, *response.Response) { +func getSharesList(ctx context.Context, client GatewayClient) (*collaboration.ListReceivedSharesResponse, *response.Response) { shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) if err != nil { e := errors.Wrap(err, "error getting shares list") diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go new file mode 100644 index 0000000000..f080c724bb --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go @@ -0,0 +1,298 @@ +// 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 shares_test + +import ( + "context" + "net/http/httptest" + + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/config" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/mock" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("The ocs API", func() { + var ( + h *shares.Handler + client *mocks.GatewayClient + ) + + BeforeEach(func() { + h = &shares.Handler{} + client = &mocks.GatewayClient{} + + c := &config.Config{} + c.Init() + h.Init(c, func() (shares.GatewayClient, error) { + return client, nil + }) + }) + + Describe("AcceptReceivedShare", func() { + type responseShare struct { + Id string `xml:"id"` + } + type listSharesData struct { + Shares []responseShare `xml:"element"` + } + type listSharesResponse struct { + Data listSharesData `xml:"data"` + } + + var ( + resId = &provider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "share1", + } + otherResId = &provider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "share3", + } + share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "1"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + }, + ResourceId: resId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + } + share2 = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "2"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_GROUP, + }, + ResourceId: resId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + } + share3 = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "4"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_GROUP, + }, + ResourceId: otherResId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + } + ) + + BeforeEach(func() { + client.On("GetShare", mock.Anything, mock.Anything).Return(&collaboration.GetShareResponse{ + Status: status.NewOK(context.Background()), + Share: share, + }, nil) + + client.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1", + Id: resId, + PermissionSet: &provider.ResourcePermissions{ + Stat: true, + }, + Size: 10, + }, + }, nil) + + client.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1", + Id: resId, + Size: 1, + }, + }, + }, nil) + }) + + Context("with one pending share", func() { + BeforeEach(func() { + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share, + MountPoint: &provider.Reference{Path: "share1"}, + }, + }, + }, nil) + }) + + It("accepts shares", func() { + client.On("UpdateReceivedShare", mock.Anything, mock.MatchedBy(func(req *collaboration.UpdateReceivedShareRequest) bool { + return req.Share.Share.Id.OpaqueId == "1" + })).Return(&collaboration.UpdateReceivedShareResponse{ + Status: status.NewOK(context.Background()), + Share: &collaboration.ReceivedShare{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share, + MountPoint: &provider.Reference{Path: "share1"}, + }, + }, nil) + + req := httptest.NewRequest("POST", "/apps/files_sharing/api/v1/shares/pending/1", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("shareid", "1") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + w := httptest.NewRecorder() + h.AcceptReceivedShare(w, req) + Expect(w.Result().StatusCode).To(Equal(200)) + + client.AssertNumberOfCalls(GinkgoT(), "UpdateReceivedShare", 1) + }) + }) + + Context("with two pending shares for the same resource", func() { + BeforeEach(func() { + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share, + MountPoint: &provider.Reference{Path: "share1"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share2, + MountPoint: &provider.Reference{Path: "share2"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share3, + MountPoint: &provider.Reference{Path: "share3"}, + }, + }, + }, nil) + }) + + It("accepts both pending shares", func() { + client.On("UpdateReceivedShare", mock.Anything, mock.MatchedBy(func(req *collaboration.UpdateReceivedShareRequest) bool { + return req.Share.Share.Id.OpaqueId == "1" && req.Share.MountPoint.Path == "share1" + })).Return(&collaboration.UpdateReceivedShareResponse{ + Status: status.NewOK(context.Background()), + Share: &collaboration.ReceivedShare{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share, + MountPoint: &provider.Reference{Path: "share1"}, + }, + }, nil) + + client.On("UpdateReceivedShare", mock.Anything, mock.MatchedBy(func(req *collaboration.UpdateReceivedShareRequest) bool { + return req.Share.Share.Id.OpaqueId == "2" && req.Share.MountPoint.Path == "share1" + })).Return(&collaboration.UpdateReceivedShareResponse{ + Status: status.NewOK(context.Background()), + Share: &collaboration.ReceivedShare{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share2, + MountPoint: &provider.Reference{Path: "share2"}, + }, + }, nil) + + req := httptest.NewRequest("POST", "/apps/files_sharing/api/v1/shares/pending/1", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("shareid", "1") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + w := httptest.NewRecorder() + h.AcceptReceivedShare(w, req) + Expect(w.Result().StatusCode).To(Equal(200)) + + client.AssertCalled(GinkgoT(), "UpdateReceivedShare", mock.Anything, mock.Anything) + client.AssertNumberOfCalls(GinkgoT(), "UpdateReceivedShare", 2) + }) + }) + + Context("with one accepted and one pending share for the same resource", func() { + BeforeEach(func() { + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share, + MountPoint: &provider.Reference{Path: "existing/mountpoint"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share2, + MountPoint: &provider.Reference{Path: "share2"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share3, + MountPoint: &provider.Reference{Path: "share3"}, + }, + }, + }, nil) + }) + + It("accepts the remaining pending share", func() { + client.On("UpdateReceivedShare", mock.Anything, mock.MatchedBy(func(req *collaboration.UpdateReceivedShareRequest) bool { + return req.Share.Share.Id.OpaqueId == "2" && req.Share.MountPoint.Path == "existing/mountpoint" + })).Return(&collaboration.UpdateReceivedShareResponse{ + Status: status.NewOK(context.Background()), + Share: &collaboration.ReceivedShare{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share2, + MountPoint: &provider.Reference{Path: "share2"}, + }, + }, nil) + + req := httptest.NewRequest("POST", "/apps/files_sharing/api/v1/shares/pending/2", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("shareid", "2") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + w := httptest.NewRecorder() + h.AcceptReceivedShare(w, req) + Expect(w.Result().StatusCode).To(Equal(200)) + + client.AssertCalled(GinkgoT(), "UpdateReceivedShare", mock.Anything, mock.Anything) + client.AssertNumberOfCalls(GinkgoT(), "UpdateReceivedShare", 1) + }) + }) + }) +}) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/private_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/private_test.go new file mode 100644 index 0000000000..d21b7e42dc --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/private_test.go @@ -0,0 +1,72 @@ +// Copyright 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 shares +package shares + +import ( + "testing" + + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" +) + +func TestGetStateFilter(t *testing.T) { + tests := []struct { + input string + expected collaboration.ShareState + }{ + {"all", ocsStateUnknown}, + {"0", collaboration.ShareState_SHARE_STATE_ACCEPTED}, + {"1", collaboration.ShareState_SHARE_STATE_PENDING}, + {"2", collaboration.ShareState_SHARE_STATE_REJECTED}, + {"something_invalid", collaboration.ShareState_SHARE_STATE_ACCEPTED}, + {"", collaboration.ShareState_SHARE_STATE_ACCEPTED}, + } + + for _, tt := range tests { + state := getStateFilter(tt.input) + if state != tt.expected { + t.Errorf("getStateFilter(\"%s\") returned %s instead of expected %s", tt.input, state, tt.expected) + } + } +} + +func TestMapState(t *testing.T) { + // case collaboration.ShareState_SHARE_STATE_PENDING: + // mapped = ocsStatePending + // case collaboration.ShareState_SHARE_STATE_ACCEPTED: + // mapped = ocsStateAccepted + // case collaboration.ShareState_SHARE_STATE_REJECTED: + // mapped = ocsStateRejected + // default: + // mapped = ocsStateUnknown + tests := []struct { + input collaboration.ShareState + expected int + }{ + {collaboration.ShareState_SHARE_STATE_PENDING, ocsStatePending}, + {collaboration.ShareState_SHARE_STATE_ACCEPTED, ocsStateAccepted}, + {collaboration.ShareState_SHARE_STATE_REJECTED, ocsStateRejected}, + {42, ocsStateUnknown}, + } + + for _, tt := range tests { + state := mapState(tt.input) + if state != tt.expected { + t.Errorf("mapState(%d) returned %d instead of expected %d", tt.input, state, tt.expected) + } + } +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index d3925dd744..78a54b5a51 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -32,7 +32,6 @@ import ( "text/template" "time" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -41,6 +40,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" + grpc "google.golang.org/grpc" "github.com/ReneKroon/ttlcache/v2" "github.com/bluele/gcache" @@ -58,6 +58,8 @@ import ( "github.com/pkg/errors" ) +//go:generate mockery -name GatewayClient + const ( storageIDPrefix string = "shared::" ) @@ -72,6 +74,8 @@ type Handler struct { userIdentifierCache *ttlcache.Cache resourceInfoCache gcache.Cache resourceInfoCacheTTL time.Duration + + getClient GatewayClientGetter } // we only cache the minimal set of data instead of the full user metadata @@ -88,8 +92,22 @@ func getCacheWarmupManager(c *config.Config) (cache.Warmup, error) { return nil, fmt.Errorf("driver not found: %s", c.CacheWarmupDriver) } +type GatewayClientGetter func() (GatewayClient, error) +type GatewayClient interface { + Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) + ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) + + GetShare(ctx context.Context, in *collaboration.GetShareRequest, opts ...grpc.CallOption) (*collaboration.GetShareResponse, error) + CreateShare(ctx context.Context, in *collaboration.CreateShareRequest, opts ...grpc.CallOption) (*collaboration.CreateShareResponse, error) + ListReceivedShares(ctx context.Context, in *collaboration.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaboration.ListReceivedSharesResponse, error) + UpdateReceivedShare(ctx context.Context, in *collaboration.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaboration.UpdateReceivedShareResponse, error) + + GetGroup(ctx context.Context, in *grouppb.GetGroupRequest, opts ...grpc.CallOption) (*grouppb.GetGroupResponse, error) + GetUser(ctx context.Context, in *userpb.GetUserRequest, opts ...grpc.CallOption) (*userpb.GetUserResponse, error) +} + // Init initializes this and any contained handlers -func (h *Handler) Init(c *config.Config) { +func (h *Handler) InitDefault(c *config.Config) { h.gatewayAddr = c.GatewaySvc h.publicURL = c.Config.Host h.sharePrefix = c.SharePrefix @@ -108,6 +126,12 @@ func (h *Handler) Init(c *config.Config) { go h.startCacheWarmup(cwm) } } + h.getClient = h.getPoolClient +} + +func (h *Handler) Init(c *config.Config, clientGetter GatewayClientGetter) { + h.InitDefault(c) + h.getClient = clientGetter } func (h *Handler) startCacheWarmup(c cache.Warmup) { @@ -513,7 +537,7 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) { stateFilter := getStateFilter(r.FormValue("state")) log := appctx.GetLogger(r.Context()) - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + client, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return @@ -878,7 +902,7 @@ func (h *Handler) addFileInfo(ctx context.Context, s *conversions.ShareData, inf } // mustGetIdentifiers always returns a struct with identifiers, if the user or group could not be found they will all be empty -func (h *Handler) mustGetIdentifiers(ctx context.Context, client gateway.GatewayAPIClient, id string, isGroup bool) *userIdentifiers { +func (h *Handler) mustGetIdentifiers(ctx context.Context, client GatewayClient, id string, isGroup bool) *userIdentifiers { sublog := appctx.GetLogger(ctx).With().Str("id", id).Logger() if id == "" { return &userIdentifiers{} @@ -956,7 +980,7 @@ func (h *Handler) mustGetIdentifiers(ctx context.Context, client gateway.Gateway return ui } -func (h *Handler) mapUserIds(ctx context.Context, client gateway.GatewayAPIClient, s *conversions.ShareData) { +func (h *Handler) mapUserIds(ctx context.Context, client GatewayClient, s *conversions.ShareData) { if s.UIDOwner != "" { owner := h.mustGetIdentifiers(ctx, client, s.UIDOwner, false) s.UIDOwner = owner.Username @@ -1001,19 +1025,19 @@ func (h *Handler) getAdditionalInfoAttribute(ctx context.Context, u *userIdentif return buf.String() } -func (h *Handler) getResourceInfoByPath(ctx context.Context, client gateway.GatewayAPIClient, path string) (*provider.ResourceInfo, *rpc.Status, error) { +func (h *Handler) getResourceInfoByPath(ctx context.Context, client GatewayClient, path string) (*provider.ResourceInfo, *rpc.Status, error) { return h.getResourceInfo(ctx, client, path, &provider.Reference{ Path: path, }) } -func (h *Handler) getResourceInfoByID(ctx context.Context, client gateway.GatewayAPIClient, id *provider.ResourceId) (*provider.ResourceInfo, *rpc.Status, error) { +func (h *Handler) getResourceInfoByID(ctx context.Context, client GatewayClient, id *provider.ResourceId) (*provider.ResourceInfo, *rpc.Status, error) { return h.getResourceInfo(ctx, client, wrapResourceID(id), &provider.Reference{ResourceId: id}) } // getResourceInfo retrieves the resource info to a target. // This method utilizes caching if it is enabled. -func (h *Handler) getResourceInfo(ctx context.Context, client gateway.GatewayAPIClient, key string, ref *provider.Reference) (*provider.ResourceInfo, *rpc.Status, error) { +func (h *Handler) getResourceInfo(ctx context.Context, client GatewayClient, key string, ref *provider.Reference) (*provider.ResourceInfo, *rpc.Status, error) { logger := appctx.GetLogger(ctx) var pinfo *provider.ResourceInfo @@ -1047,7 +1071,7 @@ func (h *Handler) getResourceInfo(ctx context.Context, client gateway.GatewayAPI return pinfo, status, nil } -func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client gateway.GatewayAPIClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) { +func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client GatewayClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) { createShareResponse, err := client.CreateShare(ctx, req) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) @@ -1107,3 +1131,7 @@ func getStateFilter(s string) collaboration.ShareState { } return stateFilter } + +func (h *Handler) getPoolClient() (GatewayClient, error) { + return pool.GetGatewayServiceClient(h.gatewayAddr) +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go new file mode 100644 index 0000000000..800177c0b0 --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go @@ -0,0 +1,13 @@ +package shares_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestShares(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Shares Suite") +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go index d21b7e42dc..71d0f390bf 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 CERN +// 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. @@ -14,59 +14,121 @@ // // 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 shares -package shares +// or submit itself to any jurisdiction. + +package shares_test import ( - "testing" + "context" + "encoding/xml" + "net/http/httptest" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/config" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/stretchr/testify/mock" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) -func TestGetStateFilter(t *testing.T) { - tests := []struct { - input string - expected collaboration.ShareState - }{ - {"all", ocsStateUnknown}, - {"0", collaboration.ShareState_SHARE_STATE_ACCEPTED}, - {"1", collaboration.ShareState_SHARE_STATE_PENDING}, - {"2", collaboration.ShareState_SHARE_STATE_REJECTED}, - {"something_invalid", collaboration.ShareState_SHARE_STATE_ACCEPTED}, - {"", collaboration.ShareState_SHARE_STATE_ACCEPTED}, - } +var _ = Describe("The ocs API", func() { + var ( + h *shares.Handler + client *mocks.GatewayClient + ) + + BeforeEach(func() { + h = &shares.Handler{} + client = &mocks.GatewayClient{} + + c := &config.Config{} + c.Init() + h.Init(c, func() (shares.GatewayClient, error) { + return client, nil + }) + }) + + Describe("ListShares", func() { + BeforeEach(func() { + resId := &provider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "share1", + } + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "10"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + }, + ResourceId: resId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + }, + MountPoint: &provider.Reference{Path: "share1"}, + }, + }, + }, nil) + + client.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1", + Id: resId, + PermissionSet: &provider.ResourcePermissions{ + Stat: true, + }, + Size: 10, + }, + }, nil) + + client.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/share1", + Id: resId, + Size: 1, + }, + }, + }, nil) + }) - for _, tt := range tests { - state := getStateFilter(tt.input) - if state != tt.expected { - t.Errorf("getStateFilter(\"%s\") returned %s instead of expected %s", tt.input, state, tt.expected) - } - } -} + It("lists accepted shares", func() { + type share struct { + Id string `xml:"id"` + } + type data struct { + Shares []share `xml:"element"` + } + type response struct { + Data data `xml:"data"` + } -func TestMapState(t *testing.T) { - // case collaboration.ShareState_SHARE_STATE_PENDING: - // mapped = ocsStatePending - // case collaboration.ShareState_SHARE_STATE_ACCEPTED: - // mapped = ocsStateAccepted - // case collaboration.ShareState_SHARE_STATE_REJECTED: - // mapped = ocsStateRejected - // default: - // mapped = ocsStateUnknown - tests := []struct { - input collaboration.ShareState - expected int - }{ - {collaboration.ShareState_SHARE_STATE_PENDING, ocsStatePending}, - {collaboration.ShareState_SHARE_STATE_ACCEPTED, ocsStateAccepted}, - {collaboration.ShareState_SHARE_STATE_REJECTED, ocsStateRejected}, - {42, ocsStateUnknown}, - } + req := httptest.NewRequest("GET", "/apps/files_sharing/api/v1/shares?shared_with_me=1", nil) + w := httptest.NewRecorder() + h.ListShares(w, req) + Expect(w.Result().StatusCode).To(Equal(200)) - for _, tt := range tests { - state := mapState(tt.input) - if state != tt.expected { - t.Errorf("mapState(%d) returned %d instead of expected %d", tt.input, state, tt.expected) - } - } -} + res := &response{} + err := xml.Unmarshal(w.Body.Bytes(), res) + Expect(err).ToNot(HaveOccurred()) + Expect(len(res.Data.Shares)).To(Equal(1)) + s := res.Data.Shares[0] + Expect(s.Id).To(Equal("10")) + }) + }) +}) diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index d411f4dace..6e08752114 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -96,7 +96,7 @@ func (s *svc) routerInit() error { capabilitiesHandler.Init(s.c) usersHandler.Init(s.c) configHandler.Init(s.c) - sharesHandler.Init(s.c) + sharesHandler.InitDefault(s.c) shareesHandler.Init(s.c) s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) { From 37e494490c19e5a3fa7e3b3de342a7d89fba12b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 15 Oct 2021 10:57:59 +0200 Subject: [PATCH 11/30] Auto-accept a share if there already is an accepted one to that resource --- .../sharing/shares/mocks/GatewayClient.go | 90 ++++++++++ .../handlers/apps/sharing/shares/shares.go | 58 ++++++- .../apps/sharing/shares/shares_test.go | 159 ++++++++++++++++++ .../ocs/handlers/apps/sharing/shares/user.go | 19 +-- 4 files changed, 307 insertions(+), 19 deletions(-) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go index fd04a523bc..247bf5f8ff 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -143,6 +143,36 @@ func (_m *GatewayClient) GetUser(ctx context.Context, in *userv1beta1.GetUserReq return r0, r1 } +// GetUserByClaim provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) GetUserByClaim(ctx context.Context, in *userv1beta1.GetUserByClaimRequest, opts ...grpc.CallOption) (*userv1beta1.GetUserByClaimResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *userv1beta1.GetUserByClaimResponse + if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.GetUserByClaimRequest, ...grpc.CallOption) *userv1beta1.GetUserByClaimResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*userv1beta1.GetUserByClaimResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.GetUserByClaimRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListContainer provides a mock function with given fields: ctx, in, opts func (_m *GatewayClient) ListContainer(ctx context.Context, in *providerv1beta1.ListContainerRequest, opts ...grpc.CallOption) (*providerv1beta1.ListContainerResponse, error) { _va := make([]interface{}, len(opts)) @@ -203,6 +233,66 @@ func (_m *GatewayClient) ListReceivedShares(ctx context.Context, in *collaborati return r0, r1 } +// ListShares provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) ListShares(ctx context.Context, in *collaborationv1beta1.ListSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListSharesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.ListSharesResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ListSharesRequest, ...grpc.CallOption) *collaborationv1beta1.ListSharesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.ListSharesResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.ListSharesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveShare provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) RemoveShare(ctx context.Context, in *collaborationv1beta1.RemoveShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.RemoveShareResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *collaborationv1beta1.RemoveShareResponse + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.RemoveShareRequest, ...grpc.CallOption) *collaborationv1beta1.RemoveShareResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*collaborationv1beta1.RemoveShareResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.RemoveShareRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Stat provides a mock function with given fields: ctx, in, opts func (_m *GatewayClient) Stat(ctx context.Context, in *providerv1beta1.StatRequest, opts ...grpc.CallOption) (*providerv1beta1.StatResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 78a54b5a51..6a81e70f74 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -41,6 +41,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" grpc "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/ReneKroon/ttlcache/v2" "github.com/bluele/gcache" @@ -97,13 +98,16 @@ type GatewayClient interface { Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) + ListShares(ctx context.Context, in *collaboration.ListSharesRequest, opts ...grpc.CallOption) (*collaboration.ListSharesResponse, error) GetShare(ctx context.Context, in *collaboration.GetShareRequest, opts ...grpc.CallOption) (*collaboration.GetShareResponse, error) CreateShare(ctx context.Context, in *collaboration.CreateShareRequest, opts ...grpc.CallOption) (*collaboration.CreateShareResponse, error) + RemoveShare(ctx context.Context, in *collaboration.RemoveShareRequest, opts ...grpc.CallOption) (*collaboration.RemoveShareResponse, error) ListReceivedShares(ctx context.Context, in *collaboration.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaboration.ListReceivedSharesResponse, error) UpdateReceivedShare(ctx context.Context, in *collaboration.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaboration.UpdateReceivedShareResponse, error) GetGroup(ctx context.Context, in *grouppb.GetGroupRequest, opts ...grpc.CallOption) (*grouppb.GetGroupResponse, error) GetUser(ctx context.Context, in *userpb.GetUserRequest, opts ...grpc.CallOption) (*userpb.GetUserResponse, error) + GetUserByClaim(ctx context.Context, in *userpb.GetUserByClaimRequest, opts ...grpc.CallOption) (*userpb.GetUserByClaimResponse, error) } // Init initializes this and any contained handlers @@ -156,7 +160,7 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { } // get user permissions on the shared file - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + client, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return @@ -192,8 +196,43 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { switch shareType { case int(conversions.ShareTypeUser): // user collaborations default to coowner - if role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()); err == nil { - h.createUserShare(w, r, statRes.Info, role, val) + role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()) + if err != nil { + return + } + + share := h.createUserShare(w, r, statRes.Info, role, val) + if share == nil { + return + } + + lrs, ocsResponse := getSharesList(ctx, client) + if ocsResponse != nil { + response.WriteOCSResponse(w, r, *ocsResponse, nil) + return + } + + for _, s := range lrs.Shares { + if s.GetShare().GetId() != share.Id && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED && utils.ResourceIDEqual(s.Share.ResourceId, statRes.Info.GetId()) { + updateRequest := &collaboration.UpdateReceivedShareRequest{ + Share: &collaboration.ReceivedShare{ + Share: share, + MountPoint: s.MountPoint, + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state, mount_point"}}, + } + + shareRes, err := client.UpdateReceivedShare(ctx, updateRequest) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err) + } + + if shareRes.Status.Code != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err) + } + return + } } case int(conversions.ShareTypeGroup): // group collaborations default to coowner @@ -1071,33 +1110,34 @@ func (h *Handler) getResourceInfo(ctx context.Context, client GatewayClient, key return pinfo, status, nil } -func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client GatewayClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) { +func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client GatewayClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) *collaboration.Share { createShareResponse, err := client.CreateShare(ctx, req) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) - return + return nil } if createShareResponse.Status.Code != rpc.Code_CODE_OK { if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return + return nil } response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) - return + return nil } s, err := conversions.CS3Share2ShareData(ctx, createShareResponse.Share) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return + return nil } err = h.addFileInfo(ctx, s, info) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return + return nil } h.mapUserIds(ctx, client, s) response.WriteOCSSuccess(w, r, s) + return createShareResponse.Share } func mapState(state collaboration.ShareState) int { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go index 71d0f390bf..9dd9650342 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go @@ -21,8 +21,12 @@ package shares_test import ( "context" "encoding/xml" + "fmt" "net/http/httptest" + "net/url" + "strings" + 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" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/config" @@ -52,6 +56,161 @@ var _ = Describe("The ocs API", func() { }) }) + Describe("CreateShare", func() { + var ( + resId = &provider.ResourceId{ + StorageId: "share1-storageid", + OpaqueId: "share1", + } + share = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "1"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + }, + ResourceId: resId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + } + share2 = &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: "2"}, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + }, + ResourceId: resId, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + Stat: true, + ListContainer: true, + }, + }, + } + ) + + BeforeEach(func() { + client.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{ + Status: status.NewOK(context.Background()), + Info: &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: "/newshare", + Id: resId, + PermissionSet: &provider.ResourcePermissions{ + Stat: true, + AddGrant: true, + UpdateGrant: true, + RemoveGrant: true, + }, + Size: 10, + }, + }, nil) + + client.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userpb.GetUserByClaimResponse{ + Status: status.NewOK(context.Background()), + User: &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: "admin", + }, + }, + }, nil) + + client.On("GetShare", mock.Anything, mock.Anything).Return(&collaboration.GetShareResponse{ + Status: status.NewOK(context.Background()), + Share: share, + }, nil) + }) + + Context("when there are no existing shares to the resource yet", func() { + BeforeEach(func() { + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share, + MountPoint: &provider.Reference{Path: ""}, + }, + }, + }, nil) + }) + + It("creates a new share", func() { + client.On("CreateShare", mock.Anything, mock.Anything).Return(&collaboration.CreateShareResponse{ + Status: status.NewOK(context.Background()), + Share: share, + }, nil) + + form := url.Values{} + form.Add("shareType", "0") + form.Add("path", "/newshare") + form.Add("name", "newshare") + form.Add("permissions", "16") + form.Add("shareWith", "admin") + req := httptest.NewRequest("POST", "/apps/files_sharing/api/v1/shares", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + w := httptest.NewRecorder() + h.CreateShare(w, req) + fmt.Println(w.Result()) + Expect(w.Result().StatusCode).To(Equal(200)) + client.AssertNumberOfCalls(GinkgoT(), "CreateShare", 1) + }) + }) + + Context("when a share to the same resource already exists", func() { + BeforeEach(func() { + client.On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything).Return(&collaboration.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaboration.ReceivedShare{ + { + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share, + MountPoint: &provider.Reference{Path: "some-mountpoint"}, + }, + { + State: collaboration.ShareState_SHARE_STATE_PENDING, + Share: share2, + }, + }, + }, nil) + }) + + It("auto-accepts the share and applies the mountpoint", func() { + client.On("CreateShare", mock.Anything, mock.Anything).Return(&collaboration.CreateShareResponse{ + Status: status.NewOK(context.Background()), + Share: share2, + }, nil) + client.On("UpdateReceivedShare", mock.Anything, mock.MatchedBy(func(req *collaboration.UpdateReceivedShareRequest) bool { + return req.Share.Share.Id.OpaqueId == "2" && req.Share.MountPoint.Path == "some-mountpoint" && req.Share.State == collaboration.ShareState_SHARE_STATE_ACCEPTED + })).Return(&collaboration.UpdateReceivedShareResponse{ + Status: status.NewOK(context.Background()), + Share: &collaboration.ReceivedShare{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + Share: share2, + MountPoint: &provider.Reference{Path: "share2"}, + }, + }, nil) + + form := url.Values{} + form.Add("shareType", "0") + form.Add("path", "/newshare") + form.Add("name", "newshare") + form.Add("permissions", "16") + form.Add("shareWith", "admin") + req := httptest.NewRequest("POST", "/apps/files_sharing/api/v1/shares", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + w := httptest.NewRecorder() + h.CreateShare(w, req) + Expect(w.Result().StatusCode).To(Equal(200)) + client.AssertNumberOfCalls(GinkgoT(), "CreateShare", 1) + client.AssertNumberOfCalls(GinkgoT(), "UpdateReceivedShare", 1) + }) + }) + }) + Describe("ListShares", func() { BeforeEach(func() { resId := &provider.ResourceId{ diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 22cc7ef477..9326edaf61 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -30,21 +30,20 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) -func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) { +func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) *collaboration.Share { ctx := r.Context() - c, err := pool.GetGatewayServiceClient(h.gatewayAddr) + c, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) - return + return nil } shareWith := r.FormValue("shareWith") if shareWith == "" { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil) - return + return nil } userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ @@ -53,12 +52,12 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) - return + return nil } if userRes.Status.Code != rpc.Code_CODE_OK { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err) - return + return nil } createShareReq := &collaboration.CreateShareRequest{ @@ -82,13 +81,13 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + return h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) } func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, shareID string) { ctx := r.Context() - uClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) + uClient, err := h.getClient() if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return @@ -152,7 +151,7 @@ func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filte ocsDataPayload := make([]*conversions.ShareData, 0) if h.gatewayAddr != "" { // get a connection to the users share provider - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + client, err := h.getClient() if err != nil { return ocsDataPayload, nil, err } From a9bb9fa41b114c4eb1084f3952a58f5e3fe3bac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 15 Oct 2021 14:40:52 +0200 Subject: [PATCH 12/30] Make hound happy --- .../handlers/apps/sharing/shares/pending.go | 2 +- .../apps/sharing/shares/pending_test.go | 16 +++++++-------- .../handlers/apps/sharing/shares/shares.go | 6 +++++- .../apps/sharing/shares/shares_test.go | 20 +++++++++---------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go index 0697018735..0a4f814654 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending.go @@ -99,7 +99,7 @@ func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) { } } - for id, _ := range sharesToAccept { + for id := range sharesToAccept { h.updateReceivedShare(w, r, id, false, mount) } } diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go index f080c724bb..01a322e259 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go @@ -54,7 +54,7 @@ var _ = Describe("The ocs API", func() { Describe("AcceptReceivedShare", func() { type responseShare struct { - Id string `xml:"id"` + ID string `xml:"id"` } type listSharesData struct { Shares []responseShare `xml:"element"` @@ -64,11 +64,11 @@ var _ = Describe("The ocs API", func() { } var ( - resId = &provider.ResourceId{ + resID = &provider.ResourceId{ StorageId: "share1-storageid", OpaqueId: "share1", } - otherResId = &provider.ResourceId{ + otherResID = &provider.ResourceId{ StorageId: "share1-storageid", OpaqueId: "share3", } @@ -77,7 +77,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_USER, }, - ResourceId: resId, + ResourceId: resID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -90,7 +90,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_GROUP, }, - ResourceId: resId, + ResourceId: resID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -103,7 +103,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_GROUP, }, - ResourceId: otherResId, + ResourceId: otherResID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -124,7 +124,7 @@ var _ = Describe("The ocs API", func() { Info: &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, Path: "/share1", - Id: resId, + Id: resID, PermissionSet: &provider.ResourcePermissions{ Stat: true, }, @@ -138,7 +138,7 @@ var _ = Describe("The ocs API", func() { { Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, Path: "/share1", - Id: resId, + Id: resID, Size: 1, }, }, diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 6a81e70f74..97afef9857 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -93,7 +93,10 @@ func getCacheWarmupManager(c *config.Config) (cache.Warmup, error) { return nil, fmt.Errorf("driver not found: %s", c.CacheWarmupDriver) } +// GatewayClientGetter is the function being used to retrieve a gateway client instance type GatewayClientGetter func() (GatewayClient, error) + +// GatewayClient is the interface to the gateway service type GatewayClient interface { Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) @@ -110,7 +113,7 @@ type GatewayClient interface { GetUserByClaim(ctx context.Context, in *userpb.GetUserByClaimRequest, opts ...grpc.CallOption) (*userpb.GetUserByClaimResponse, error) } -// Init initializes this and any contained handlers +// InitDefault initializes the handler using default values func (h *Handler) InitDefault(c *config.Config) { h.gatewayAddr = c.GatewaySvc h.publicURL = c.Config.Host @@ -133,6 +136,7 @@ func (h *Handler) InitDefault(c *config.Config) { h.getClient = h.getPoolClient } +// Init initializes the handler func (h *Handler) Init(c *config.Config, clientGetter GatewayClientGetter) { h.InitDefault(c) h.getClient = clientGetter diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go index 9dd9650342..b04ca576e7 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go @@ -58,7 +58,7 @@ var _ = Describe("The ocs API", func() { Describe("CreateShare", func() { var ( - resId = &provider.ResourceId{ + resID = &provider.ResourceId{ StorageId: "share1-storageid", OpaqueId: "share1", } @@ -67,7 +67,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_USER, }, - ResourceId: resId, + ResourceId: resID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -80,7 +80,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_USER, }, - ResourceId: resId, + ResourceId: resID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -96,7 +96,7 @@ var _ = Describe("The ocs API", func() { Info: &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, Path: "/newshare", - Id: resId, + Id: resID, PermissionSet: &provider.ResourcePermissions{ Stat: true, AddGrant: true, @@ -213,7 +213,7 @@ var _ = Describe("The ocs API", func() { Describe("ListShares", func() { BeforeEach(func() { - resId := &provider.ResourceId{ + resID := &provider.ResourceId{ StorageId: "share1-storageid", OpaqueId: "share1", } @@ -227,7 +227,7 @@ var _ = Describe("The ocs API", func() { Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_USER, }, - ResourceId: resId, + ResourceId: resID, Permissions: &collaboration.SharePermissions{ Permissions: &provider.ResourcePermissions{ Stat: true, @@ -245,7 +245,7 @@ var _ = Describe("The ocs API", func() { Info: &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, Path: "/share1", - Id: resId, + Id: resID, PermissionSet: &provider.ResourcePermissions{ Stat: true, }, @@ -259,7 +259,7 @@ var _ = Describe("The ocs API", func() { { Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, Path: "/share1", - Id: resId, + Id: resID, Size: 1, }, }, @@ -268,7 +268,7 @@ var _ = Describe("The ocs API", func() { It("lists accepted shares", func() { type share struct { - Id string `xml:"id"` + ID string `xml:"id"` } type data struct { Shares []share `xml:"element"` @@ -287,7 +287,7 @@ var _ = Describe("The ocs API", func() { Expect(err).ToNot(HaveOccurred()) Expect(len(res.Data.Shares)).To(Equal(1)) s := res.Data.Shares[0] - Expect(s.Id).To(Equal("10")) + Expect(s.ID).To(Equal("10")) }) }) }) From ed66df40d8dcccd0146242cf90a876b9eaa385bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 22 Oct 2021 09:13:10 +0200 Subject: [PATCH 13/30] Make a "machine" type authprovider available in the acceptance tests --- .drone.star | 1 + tests/oc-integration-tests/drone/frontend.toml | 1 + tests/oc-integration-tests/drone/gateway.toml | 1 + tests/oc-integration-tests/drone/machine-auth.toml | 14 ++++++++++++++ tests/oc-integration-tests/local/frontend.toml | 1 + tests/oc-integration-tests/local/gateway.toml | 1 + tests/oc-integration-tests/local/machine-auth.toml | 14 ++++++++++++++ 7 files changed, 33 insertions(+) create mode 100644 tests/oc-integration-tests/drone/machine-auth.toml create mode 100644 tests/oc-integration-tests/local/machine-auth.toml diff --git a/.drone.star b/.drone.star index 9a9799f41e..4979688dd6 100644 --- a/.drone.star +++ b/.drone.star @@ -689,6 +689,7 @@ def ocisIntegrationTests(parallelRuns, skipExceptParts = []): "/drone/src/cmd/revad/revad -c storage-publiclink-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-shares-ocis.toml &", "/drone/src/cmd/revad/revad -c ldap-users.toml", + "/drone/src/cmd/revad/revad -c machine-auth.toml", ], }, cloneOc10TestReposStep(), diff --git a/tests/oc-integration-tests/drone/frontend.toml b/tests/oc-integration-tests/drone/frontend.toml index b0424003d9..95dd505ba9 100644 --- a/tests/oc-integration-tests/drone/frontend.toml +++ b/tests/oc-integration-tests/drone/frontend.toml @@ -61,6 +61,7 @@ files_namespace = "/users/{{.Id.OpaqueId}}" webdav_namespace = "/home" [http.services.ocs] +machine_auth_apikey = "change-me-please" [http.services.ocs.capabilities.capabilities.core.status] version = "10.0.11.5" diff --git a/tests/oc-integration-tests/drone/gateway.toml b/tests/oc-integration-tests/drone/gateway.toml index c339b730bf..8228a17377 100644 --- a/tests/oc-integration-tests/drone/gateway.toml +++ b/tests/oc-integration-tests/drone/gateway.toml @@ -53,6 +53,7 @@ driver = "static" publicshares = "localhost:17000" # started with the shares.toml basic = "localhost:18000" # started with the users.toml bearer = "localhost:20099" # started with the frontend.toml +machine = "localhost:21000" # started with the machine-auth.toml [grpc.services.storageregistry] driver = "static" diff --git a/tests/oc-integration-tests/drone/machine-auth.toml b/tests/oc-integration-tests/drone/machine-auth.toml new file mode 100644 index 0000000000..d5a3f5498d --- /dev/null +++ b/tests/oc-integration-tests/drone/machine-auth.toml @@ -0,0 +1,14 @@ +[shared] +jwt_secret = "Pive-Fumkiu4" + +# This users.toml config file will start a reva service that: +# - handles "machine" type authentication +[grpc] +address = "0.0.0.0:21000" + +[grpc.services.authprovider] +auth_manager = "machine" + +[grpc.services.authprovider.auth_managers.machine] +api_key="change-me-please" +gateway_addr="0.0.0.0:19000" \ No newline at end of file diff --git a/tests/oc-integration-tests/local/frontend.toml b/tests/oc-integration-tests/local/frontend.toml index ef2dc35507..4aec478e17 100644 --- a/tests/oc-integration-tests/local/frontend.toml +++ b/tests/oc-integration-tests/local/frontend.toml @@ -61,6 +61,7 @@ files_namespace = "/users/{{.Id.OpaqueId}}" webdav_namespace = "/home" [http.services.ocs] +machine_auth_apikey = "change-me-please" [http.services.ocs.capabilities.capabilities.core.status] version = "10.0.11.5" diff --git a/tests/oc-integration-tests/local/gateway.toml b/tests/oc-integration-tests/local/gateway.toml index 977ec1bee9..b0938de0c5 100644 --- a/tests/oc-integration-tests/local/gateway.toml +++ b/tests/oc-integration-tests/local/gateway.toml @@ -46,6 +46,7 @@ driver = "static" publicshares = "localhost:17000" # started with the shares.toml basic = "localhost:18000" # started with the users.toml bearer = "localhost:20099" # started with the frontend.toml +machine = "localhost:21000" # started with the machine-auth.toml [grpc.services.storageregistry] driver = "static" diff --git a/tests/oc-integration-tests/local/machine-auth.toml b/tests/oc-integration-tests/local/machine-auth.toml new file mode 100644 index 0000000000..d5a3f5498d --- /dev/null +++ b/tests/oc-integration-tests/local/machine-auth.toml @@ -0,0 +1,14 @@ +[shared] +jwt_secret = "Pive-Fumkiu4" + +# This users.toml config file will start a reva service that: +# - handles "machine" type authentication +[grpc] +address = "0.0.0.0:21000" + +[grpc.services.authprovider] +auth_manager = "machine" + +[grpc.services.authprovider.auth_managers.machine] +api_key="change-me-please" +gateway_addr="0.0.0.0:19000" \ No newline at end of file From 34027982f4224c725dfd9041f42e89db44ce533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 22 Oct 2021 09:14:59 +0200 Subject: [PATCH 14/30] Accept user shares if there's another accepted share to this resource --- .../services/owncloud/ocs/config/config.go | 1 + .../ocs/handlers/apps/sharing/shares/group.go | 39 +++- .../sharing/shares/mocks/GatewayClient.go | 30 +++ .../handlers/apps/sharing/shares/shares.go | 207 ++++++++++++------ .../ocs/handlers/apps/sharing/shares/user.go | 36 ++- 5 files changed, 228 insertions(+), 85 deletions(-) diff --git a/internal/http/services/owncloud/ocs/config/config.go b/internal/http/services/owncloud/ocs/config/config.go index 266c5308f6..d65e38caab 100644 --- a/internal/http/services/owncloud/ocs/config/config.go +++ b/internal/http/services/owncloud/ocs/config/config.go @@ -39,6 +39,7 @@ type Config struct { ResourceInfoCacheSize int `mapstructure:"resource_info_cache_size"` ResourceInfoCacheTTL int `mapstructure:"resource_info_cache_ttl"` UserIdentifierCacheTTL int `mapstructure:"user_identifier_cache_ttl"` + MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"` } // Init sets sane defaults diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go index 56663aec29..abd00d2faf 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go @@ -29,21 +29,25 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) -func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) { +func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) { ctx := r.Context() - c, err := pool.GetGatewayServiceClient(h.gatewayAddr) + c, err := h.getClient() if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error getting grpc gateway client", + Error: err, + } } shareWith := r.FormValue("shareWith") if shareWith == "" { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil) - return + return nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "missing shareWith", + } } groupRes, err := c.GetGroupByClaim(ctx, &grouppb.GetGroupByClaimRequest{ @@ -51,12 +55,18 @@ func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statI Value: shareWith, }) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error searching recipient", + Error: err, + } } if groupRes.Status.Code != rpc.Code_CODE_OK { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "group not found", err) - return + return nil, &ocsError{ + Code: response.MetaNotFound.StatusCode, + Message: "group not found", + Error: err, + } } createShareReq := &collaboration.CreateShareRequest{ @@ -80,5 +90,10 @@ func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statI }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq) + if ocsErr != nil { + return nil, ocsErr + } + + return share, nil } diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go index 247bf5f8ff..68adad9cf1 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -83,6 +83,36 @@ func (_m *GatewayClient) GetGroup(ctx context.Context, in *groupv1beta1.GetGroup return r0, r1 } +// GetGroupByClaim provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) GetGroupByClaim(ctx context.Context, in *groupv1beta1.GetGroupByClaimRequest, opts ...grpc.CallOption) (*groupv1beta1.GetGroupByClaimResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *groupv1beta1.GetGroupByClaimResponse + if rf, ok := ret.Get(0).(func(context.Context, *groupv1beta1.GetGroupByClaimRequest, ...grpc.CallOption) *groupv1beta1.GetGroupByClaimResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*groupv1beta1.GetGroupByClaimResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *groupv1beta1.GetGroupByClaimRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetShare provides a mock function with given fields: ctx, in, opts func (_m *GatewayClient) GetShare(ctx context.Context, in *collaborationv1beta1.GetShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetShareResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 97afef9857..115a398257 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -32,6 +32,7 @@ import ( "text/template" "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -41,6 +42,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" grpc "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/ReneKroon/ttlcache/v2" @@ -50,6 +52,7 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + revactx "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/share" @@ -68,6 +71,7 @@ const ( // Handler implements the shares part of the ownCloud sharing API type Handler struct { gatewayAddr string + machineAuthAPIKey string publicURL string sharePrefix string homeNamespace string @@ -86,6 +90,12 @@ type userIdentifiers struct { Mail string } +type ocsError struct { + Error error + Code int + Message string +} + func getCacheWarmupManager(c *config.Config) (cache.Warmup, error) { if f, ok := registry.NewFuncs[c.CacheWarmupDriver]; ok { return f(c.CacheWarmupDrivers[c.CacheWarmupDriver]) @@ -98,6 +108,8 @@ type GatewayClientGetter func() (GatewayClient, error) // GatewayClient is the interface to the gateway service type GatewayClient interface { + Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error) + Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) @@ -109,6 +121,7 @@ type GatewayClient interface { UpdateReceivedShare(ctx context.Context, in *collaboration.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaboration.UpdateReceivedShareResponse, error) GetGroup(ctx context.Context, in *grouppb.GetGroupRequest, opts ...grpc.CallOption) (*grouppb.GetGroupResponse, error) + GetGroupByClaim(ctx context.Context, in *grouppb.GetGroupByClaimRequest, opts ...grpc.CallOption) (*grouppb.GetGroupByClaimResponse, error) GetUser(ctx context.Context, in *userpb.GetUserRequest, opts ...grpc.CallOption) (*userpb.GetUserResponse, error) GetUserByClaim(ctx context.Context, in *userpb.GetUserByClaimRequest, opts ...grpc.CallOption) (*userpb.GetUserByClaimResponse, error) } @@ -116,6 +129,7 @@ type GatewayClient interface { // InitDefault initializes the handler using default values func (h *Handler) InitDefault(c *config.Config) { h.gatewayAddr = c.GatewaySvc + h.machineAuthAPIKey = c.MachineAuthAPIKey h.publicURL = c.Config.Host h.sharePrefix = c.SharePrefix h.homeNamespace = c.HomeNamespace @@ -198,51 +212,103 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { } switch shareType { - case int(conversions.ShareTypeUser): + case int(conversions.ShareTypeUser), int(conversions.ShareTypeGroup): // user collaborations default to coowner - role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()) - if err != nil { + role, val, ocsErr := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()) + if ocsErr != nil { + response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error) return } - share := h.createUserShare(w, r, statRes.Info, role, val) - if share == nil { + var share *collaboration.Share + if shareType == int(conversions.ShareTypeUser) { + share, ocsErr = h.createUserShare(w, r, statRes.Info, role, val) + } else { + share, ocsErr = h.createGroupShare(w, r, statRes.Info, role, val) + if ocsErr != nil { + response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error) + return + } + } + + s, err := conversions.CS3Share2ShareData(ctx, share) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) return } - lrs, ocsResponse := getSharesList(ctx, client) - if ocsResponse != nil { - response.WriteOCSResponse(w, r, *ocsResponse, nil) + err = h.addFileInfo(ctx, s, statRes.Info) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) return } - for _, s := range lrs.Shares { - if s.GetShare().GetId() != share.Id && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED && utils.ResourceIDEqual(s.Share.ResourceId, statRes.Info.GetId()) { - updateRequest := &collaboration.UpdateReceivedShareRequest{ - Share: &collaboration.ReceivedShare{ - Share: share, - MountPoint: s.MountPoint, - State: collaboration.ShareState_SHARE_STATE_ACCEPTED, - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state, mount_point"}}, - } + h.mapUserIds(ctx, client, s) - shareRes, err := client.UpdateReceivedShare(ctx, updateRequest) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err) - } + if shareType == int(conversions.ShareTypeUser) { + res, err := client.GetUser(ctx, &userpb.GetUserRequest{ + UserId: &userpb.UserId{ + OpaqueId: share.Grantee.GetUserId().GetOpaqueId(), + }, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not look up user", err) + return + } + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "get user call failed", nil) + return + } + if res.User == nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grantee not found", nil) + return + } - if shareRes.Status.Code != rpc.Code_CODE_OK { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err) - } + // Get auth + granteeCtx := revactx.ContextSetUser(context.Background(), res.User) + + authRes, err := client.Authenticate(granteeCtx, &gateway.AuthenticateRequest{ + Type: "machine", + ClientId: res.User.Username, + ClientSecret: h.machineAuthAPIKey, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not do machine authentication", err) return } + if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "machine authentication failed", nil) + return + } + //granteeCtx = revactx.ContextSetToken(granteeCtx, authRes.Token) + granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, revactx.TokenHeader, authRes.Token) + + lrs, ocsResponse := getSharesList(granteeCtx, client) + if ocsResponse != nil { + response.WriteOCSResponse(w, r, *ocsResponse, nil) + return + } + + for _, s := range lrs.Shares { + if s.GetShare().GetId() != share.Id && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED && utils.ResourceIDEqual(s.Share.ResourceId, statRes.Info.GetId()) { + updateRequest := &collaboration.UpdateReceivedShareRequest{ + Share: &collaboration.ReceivedShare{ + Share: share, + MountPoint: s.MountPoint, + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state, mount_point"}}, + } + + shareRes, err := client.UpdateReceivedShare(granteeCtx, updateRequest) + if err != nil || shareRes.Status.Code != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err) + return + } + } + } } - case int(conversions.ShareTypeGroup): - // group collaborations default to coowner - if role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()); err == nil { - h.createGroupShare(w, r, statRes.Info, role, val) - } + response.WriteOCSSuccess(w, r, s) case int(conversions.ShareTypePublicLink): // public links default to read only if _, _, err := h.extractPermissions(w, r, statRes.Info, conversions.NewViewerRole()); err == nil { @@ -258,7 +324,7 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri *provider.ResourceInfo, defaultPermissions *conversions.Role) (*conversions.Role, []byte, error) { +func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri *provider.ResourceInfo, defaultPermissions *conversions.Role) (*conversions.Role, []byte, *ocsError) { reqRole, reqPermissions := r.FormValue("role"), r.FormValue("permissions") var role *conversions.Role @@ -273,17 +339,27 @@ func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri } else { pint, err := strconv.Atoi(reqPermissions) if err != nil { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "permissions must be an integer", nil) - return nil, nil, err + return nil, nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "permissions must be an integer", + Error: err, + } } perm, err := conversions.NewPermissions(pint) if err != nil { if err == conversions.ErrPermissionNotInRange { - response.WriteOCSError(w, r, http.StatusNotFound, err.Error(), nil) + return nil, nil, &ocsError{ + Code: http.StatusNotFound, + Message: err.Error(), + Error: err, + } } else { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), nil) + return nil, nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: err.Error(), + Error: err, + } } - return nil, nil, err } role = conversions.RoleFromOCSPermissions(perm) } @@ -295,23 +371,32 @@ func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri permissions &^= conversions.PermissionCreate permissions &^= conversions.PermissionDelete if permissions == conversions.PermissionInvalid { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "Cannot set the requested share permissions", nil) - return nil, nil, errors.New("cannot set the requested share permissions") + return nil, nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "Cannot set the requested share permissions", + Error: errors.New("cannot set the requested share permissions"), + } } } existingPermissions := conversions.RoleFromResourcePermissions(ri.PermissionSet).OCSPermissions() if permissions == conversions.PermissionInvalid || !existingPermissions.Contain(permissions) { - response.WriteOCSError(w, r, http.StatusNotFound, "Cannot set the requested share permissions", nil) - return nil, nil, errors.New("cannot set the requested share permissions") + return nil, nil, &ocsError{ + Code: http.StatusNotFound, + Message: "Cannot set the requested share permissions", + Error: errors.New("cannot set the requested share permissions"), + } } role = conversions.RoleFromOCSPermissions(permissions) roleMap := map[string]string{"name": role.Name} val, err := json.Marshal(roleMap) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not encode role", err) - return nil, nil, err + return nil, nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "could not encode role", + Error: err, + } } return role, val, nil @@ -1114,34 +1199,30 @@ func (h *Handler) getResourceInfo(ctx context.Context, client GatewayClient, key return pinfo, status, nil } -func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client GatewayClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) *collaboration.Share { +func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client GatewayClient, req *collaboration.CreateShareRequest) (*collaboration.Share, *ocsError) { createShareResponse, err := client.CreateShare(ctx, req) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) - return nil + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error sending a grpc create share request", + Error: err, + } } if createShareResponse.Status.Code != rpc.Code_CODE_OK { if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return nil + return nil, &ocsError{ + Code: response.MetaNotFound.StatusCode, + Message: "not found", + Error: nil, + } + } + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "grpc create share request failed", + Error: nil, } - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) - return nil - } - s, err := conversions.CS3Share2ShareData(ctx, createShareResponse.Share) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return nil - } - err = h.addFileInfo(ctx, s, info) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return nil } - h.mapUserIds(ctx, client, s) - - response.WriteOCSSuccess(w, r, s) - return createShareResponse.Share + return createShareResponse.Share, nil } func mapState(state collaboration.ShareState) int { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 9326edaf61..dcacdc4744 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -32,18 +32,24 @@ import ( "github.com/cs3org/reva/pkg/appctx" ) -func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) *collaboration.Share { +func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) { ctx := r.Context() c, err := h.getClient() if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) - return nil + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error getting grpc gateway client", + Error: err, + } } shareWith := r.FormValue("shareWith") if shareWith == "" { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil) - return nil + return nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "missing shareWith", + Error: err, + } } userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ @@ -51,13 +57,19 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn Value: shareWith, }) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) - return nil + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error searching recipient", + Error: err, + } } if userRes.Status.Code != rpc.Code_CODE_OK { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err) - return nil + return nil, &ocsError{ + Code: response.MetaNotFound.StatusCode, + Message: "user not found", + Error: err, + } } createShareReq := &collaboration.CreateShareRequest{ @@ -81,7 +93,11 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }, } - return h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq) + if ocsErr != nil { + return nil, ocsErr + } + return share, nil } func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, shareID string) { From 33fcecfa8a015ddff91562b8f07cd25dbf37b1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 25 Oct 2021 10:46:38 +0200 Subject: [PATCH 15/30] Fix starting all services --- .drone.star | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.star b/.drone.star index 4979688dd6..61c3fa3c70 100644 --- a/.drone.star +++ b/.drone.star @@ -688,7 +688,7 @@ def ocisIntegrationTests(parallelRuns, skipExceptParts = []): "/drone/src/cmd/revad/revad -c storage-oc-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-publiclink-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-shares-ocis.toml &", - "/drone/src/cmd/revad/revad -c ldap-users.toml", + "/drone/src/cmd/revad/revad -c ldap-users.toml &", "/drone/src/cmd/revad/revad -c machine-auth.toml", ], }, From 39126a56b33d1bd131d32d170632ca22dca91143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 25 Oct 2021 15:15:30 +0200 Subject: [PATCH 16/30] Make sure to always send a proper response when creating public shares --- .../handlers/apps/sharing/shares/public.go | 62 +++++++++++-------- .../handlers/apps/sharing/shares/shares.go | 29 +++++++-- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go index 5e5115e23e..bb97c2f0ee 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go @@ -38,26 +38,35 @@ import ( "github.com/pkg/errors" ) -func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) { +func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) (*link.PublicShare, *ocsError) { ctx := r.Context() log := appctx.GetLogger(ctx) c, err := pool.GetGatewayServiceClient(h.gatewayAddr) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error getting grpc gateway client", + Error: err, + } } err = r.ParseForm() if err != nil { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "Could not parse form from request", err) - return + return nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "Could not parse form from request", + Error: err, + } } newPermissions, err := permissionFromRequest(r, h) if err != nil { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "Could not read permission from request", err) - return + return nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: "Could not read permission from request", + Error: err, + } } if newPermissions == nil { @@ -65,8 +74,11 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, // TODO: the default might change depending on allowed permissions and configs newPermissions, err = ocPublicPermToCs3(1, h) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "Could not convert default permissions", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "Could not convert default permissions", + Error: err, + } } } @@ -94,8 +106,11 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, if expireTimeString[0] != "" { expireTime, err := conversions.ParseTimestamp(expireTimeString[0]) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "invalid datetime format", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "invalid datetime format", + Error: err, + } } if expireTime != nil { req.Grant.Expiration = expireTime @@ -114,25 +129,22 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, createRes, err := c.CreatePublicShare(ctx, &req) if err != nil { log.Debug().Err(err).Str("createShare", "shares").Msgf("error creating a public share to resource id: %v", statInfo.GetId()) - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error creating public share", fmt.Errorf("error creating a public share to resource id: %v", statInfo.GetId())) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "error creating public share", + Error: fmt.Errorf("error creating a public share to resource id: %v", statInfo.GetId()), + } } if createRes.Status.Code != rpc.Code_CODE_OK { log.Debug().Err(errors.New("create public share failed")).Str("shares", "createShare").Msgf("create public share failed with status code: %v", createRes.Status.Code.String()) - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create public share request failed", err) - return - } - - s := conversions.PublicShare2ShareData(createRes.Share, r, h.publicURL) - err = h.addFileInfo(ctx, s, statInfo) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error enhancing response with share data", err) - return + return nil, &ocsError{ + Code: response.MetaServerError.StatusCode, + Message: "grpc create public share request failed", + Error: nil, + } } - h.mapUserIds(ctx, c, s) - - response.WriteOCSSuccess(w, r, s) + return createRes.Share, nil } func (h *Handler) listPublicShares(r *http.Request, filters []*link.ListPublicSharesRequest_Filter) ([]*conversions.ShareData, *rpc.Status, error) { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 115a398257..f7ce22c5cd 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -225,10 +225,10 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { share, ocsErr = h.createUserShare(w, r, statRes.Info, role, val) } else { share, ocsErr = h.createGroupShare(w, r, statRes.Info, role, val) - if ocsErr != nil { - response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error) - return - } + } + if ocsErr != nil { + response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error) + return } s, err := conversions.CS3Share2ShareData(ctx, share) @@ -311,9 +311,26 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { response.WriteOCSSuccess(w, r, s) case int(conversions.ShareTypePublicLink): // public links default to read only - if _, _, err := h.extractPermissions(w, r, statRes.Info, conversions.NewViewerRole()); err == nil { - h.createPublicLinkShare(w, r, statRes.Info) + _, _, ocsErr := h.extractPermissions(w, r, statRes.Info, conversions.NewViewerRole()) + if ocsErr != nil { + response.WriteOCSError(w, r, http.StatusNotFound, "No share permission", nil) + return + } + share, ocsErr := h.createPublicLinkShare(w, r, statRes.Info) + if ocsErr != nil { + response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error) + return + } + + s := conversions.PublicShare2ShareData(share, r, h.publicURL) + err = h.addFileInfo(ctx, s, statRes.Info) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error enhancing response with share data", err) + return } + h.mapUserIds(ctx, client, s) + + response.WriteOCSSuccess(w, r, s) case int(conversions.ShareTypeFederatedCloudShare): // federated shares default to read only if role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewViewerRole()); err == nil { From 1cdc1832c5ab681da926720b69dfa9f190d1f7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 25 Oct 2021 15:31:29 +0200 Subject: [PATCH 17/30] Fix s3ng acceptance tests --- .drone.star | 6 ++++-- .../drone/{storage-shares-ocis.toml => storage-shares.toml} | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename tests/oc-integration-tests/drone/{storage-shares-ocis.toml => storage-shares.toml} (100%) diff --git a/.drone.star b/.drone.star index 61c3fa3c70..9d941bf174 100644 --- a/.drone.star +++ b/.drone.star @@ -687,7 +687,7 @@ def ocisIntegrationTests(parallelRuns, skipExceptParts = []): "/drone/src/cmd/revad/revad -c storage-home-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-oc-ocis.toml &", "/drone/src/cmd/revad/revad -c storage-publiclink-ocis.toml &", - "/drone/src/cmd/revad/revad -c storage-shares-ocis.toml &", + "/drone/src/cmd/revad/revad -c storage-shares.toml &", "/drone/src/cmd/revad/revad -c ldap-users.toml &", "/drone/src/cmd/revad/revad -c machine-auth.toml", ], @@ -764,7 +764,9 @@ def s3ngIntegrationTests(parallelRuns, skipExceptParts = []): "/drone/src/cmd/revad/revad -c storage-home-s3ng.toml &", "/drone/src/cmd/revad/revad -c storage-oc-s3ng.toml &", "/drone/src/cmd/revad/revad -c storage-publiclink-s3ng.toml &", - "/drone/src/cmd/revad/revad -c ldap-users.toml", + "/drone/src/cmd/revad/revad -c storage-shares.toml &", + "/drone/src/cmd/revad/revad -c ldap-users.toml &", + "/drone/src/cmd/revad/revad -c machine-auth.toml", ], }, cloneOc10TestReposStep(), diff --git a/tests/oc-integration-tests/drone/storage-shares-ocis.toml b/tests/oc-integration-tests/drone/storage-shares.toml similarity index 100% rename from tests/oc-integration-tests/drone/storage-shares-ocis.toml rename to tests/oc-integration-tests/drone/storage-shares.toml From 788120521733cef155e015065271c481736c01c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 25 Oct 2021 16:11:54 +0200 Subject: [PATCH 18/30] Adapt expected failures --- .../expected-failures-on-S3NG-storage.md | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index 10ebba0881..d51fb718bf 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -21,14 +21,6 @@ Basic file management like up and download, move, copy, properties, quota, trash - [apiWebdavProperties1/copyFile.feature:227](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L227) - [apiWebdavProperties1/copyFile.feature:244](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L244) - [apiWebdavProperties1/copyFile.feature:245](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L245) -- [apiWebdavProperties1/copyFile.feature:267](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L267) -- [apiWebdavProperties1/copyFile.feature:268](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L268) -- [apiWebdavProperties1/copyFile.feature:292](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L292) -- [apiWebdavProperties1/copyFile.feature:293](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L293) -- [apiWebdavProperties1/copyFile.feature:316](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L316) -- [apiWebdavProperties1/copyFile.feature:317](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L317) -- [apiWebdavProperties1/copyFile.feature:340](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L340) -- [apiWebdavProperties1/copyFile.feature:341](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L341) #### [Custom dav properties with namespaces are rendered incorrectly](https://github.com/owncloud/ocis/issues/2140) @@ -522,12 +514,6 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt - [apiTrashbin/trashbinSharingToShares.feature:154](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinSharingToShares.feature#L154) - [apiTrashbin/trashbinSharingToShares.feature:155](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinSharingToShares.feature#L155) -#### [Folder overwrite on shared files doesn't works correctly on copying file](https://github.com/owncloud/ocis/issues/2183) -- [apiWebdavProperties1/copyFile.feature:409](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L409) -- [apiWebdavProperties1/copyFile.feature:410](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L410) -- [apiWebdavProperties1/copyFile.feature:491](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L491) -- [apiWebdavProperties1/copyFile.feature:492](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L492) - #### [changing user quota gives ocs status 103 / cannot set user quota using the ocs endpoint](https://github.com/owncloud/product/issues/247) _getting and setting quota_ _requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ @@ -666,7 +652,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p #### [Expiration date for shares is not implemented](https://github.com/owncloud/ocis/issues/1250) #### Expiration date of user shares- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:29](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L29) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:30](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L30) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:58](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L58) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:59](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L59) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:86](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L86) @@ -675,8 +660,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:114](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L114) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:140](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L140) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:141](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L141) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L162) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L163) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:303](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L303) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:304](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L304) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:325](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L325) @@ -689,10 +672,6 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:389](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L389) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:406](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L406) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:407](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L407) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:566](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L566) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:567](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L567) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:584](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L584) -- [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:585](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L585) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:606](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L606) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:607](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L607) - [apiShareCreateSpecialToShares1/createShareExpirationDate.feature:631](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareExpirationDate.feature#L631) @@ -734,38 +713,20 @@ Scenario Outline: Renaming a file to a path with extension .part should not be p - [apiShareReshareToShares3/reShareWithExpiryDate.feature:37](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L37) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:92](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L92) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:93](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L93) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:94](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L94) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:95](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L95) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:124](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L124) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:125](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L125) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:126](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L126) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:127](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L127) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:153](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L153) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:154](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L154) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:155](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L155) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:156](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L156) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:186](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L186) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:187](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L187) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:215](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L215) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:216](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L216) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:217](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L217) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:218](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L218) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:273](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L273) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:274](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L274) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:275](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L275) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:276](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L276) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:305](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L305) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:306](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L306) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:307](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L307) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:308](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L308) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:338](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L338) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:339](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L339) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:340](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L340) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:341](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L341) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:368](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L368) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:369](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L369) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:370](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L370) -- [apiShareReshareToShares3/reShareWithExpiryDate.feature:371](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L371) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:403](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L403) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:404](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L404) - [apiShareReshareToShares3/reShareWithExpiryDate.feature:405](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareReshareToShares3/reShareWithExpiryDate.feature#L405) @@ -977,12 +938,6 @@ And other missing implementation of favorites - [apiFavorites/favorites.feature:149](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L149) - [apiFavorites/favorites.feature:176](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L176) - [apiFavorites/favorites.feature:177](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L177) -- [apiFavorites/favoritesSharingToShares.feature:21](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L21) -- [apiFavorites/favoritesSharingToShares.feature:22](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L22) -- [apiFavorites/favoritesSharingToShares.feature:35](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L35) -- [apiFavorites/favoritesSharingToShares.feature:36](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L36) -- [apiFavorites/favoritesSharingToShares.feature:48](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L48) -- [apiFavorites/favoritesSharingToShares.feature:49](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L49) - [apiFavorites/favoritesSharingToShares.feature:62](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L62) - [apiFavorites/favoritesSharingToShares.feature:63](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favoritesSharingToShares.feature#L63) @@ -1324,10 +1279,6 @@ _ocs: api compatibility, return correct status code_ - [apiWebdavProperties1/copyFile.feature:363](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L363) - [apiWebdavProperties1/copyFile.feature:383](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L483) - [apiWebdavProperties1/copyFile.feature:384](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L484) -- [apiWebdavProperties1/copyFile.feature:437](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L437) -- [apiWebdavProperties1/copyFile.feature:438](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L438) -- [apiWebdavProperties1/copyFile.feature:464](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L464) -- [apiWebdavProperties1/copyFile.feature:465](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L465) ### [not possible to overwrite a received shared file](https://github.com/owncloud/ocis/issues/2267) - [apiShareOperationsToShares1/changingFilesShare.feature:114](https://github.com/owncloud/web/blob/master/tests/acceptance/features/apiShareOperationsToShares1/changingFilesShare.feature#L114) From 5f8edba6660b683827fefca5aede448be83dfa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Tue, 26 Oct 2021 11:27:31 +0200 Subject: [PATCH 19/30] Do not allow cross-storage restores --- .../grpc/services/gateway/storageprovider.go | 19 +++++++++++++++++++ .../http/services/owncloud/ocdav/trashbin.go | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 2db2fd8752..01babe3c91 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -1045,6 +1045,25 @@ func (s *svc) ListRecycle(ctx context.Context, req *provider.ListRecycleRequest) } func (s *svc) RestoreRecycleItem(ctx context.Context, req *provider.RestoreRecycleItemRequest) (*provider.RestoreRecycleItemResponse, error) { + sourceProviderInfo, err := s.findProviders(ctx, req.Ref) + if err != nil { + return &provider.RestoreRecycleItemResponse{ + Status: status.NewStatusFromErrType(ctx, "RestoreRecycleItem ref="+req.Ref.String(), err), + }, nil + } + destinationProviderInfo, err := s.findProviders(ctx, req.RestoreRef) + if err != nil { + return &provider.RestoreRecycleItemResponse{ + Status: status.NewStatusFromErrType(ctx, "RestoreRecycleItem ref="+req.Ref.String(), err), + }, nil + } + if sourceProviderInfo[0].ProviderId != destinationProviderInfo[0].ProviderId || + sourceProviderInfo[0].ProviderPath != destinationProviderInfo[0].ProviderPath { + return &provider.RestoreRecycleItemResponse{ + Status: status.NewPermissionDenied(ctx, err, "gateway: cross-storage restores are forbidden"), + }, nil + } + c, err := s.find(ctx, req.Ref) if err != nil { return &provider.RestoreRecycleItemResponse{ diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index 5d7425d1bd..ce16a50c89 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -145,7 +145,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { w.WriteHeader(http.StatusBadRequest) return } - dst = path.Clean(dst) + dst = path.Join(basePath, dst) log.Debug().Str("key", key).Str("dst", dst).Msg("restore") @@ -465,7 +465,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc } dstRef := &provider.Reference{ - Path: path.Join(basePath, dst), + Path: dst, } dstStatReq := &provider.StatRequest{ @@ -488,7 +488,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc // restore location exists, and if it doesn't returns a conflict error code. if dstStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND && isNested(dst) { parentStatReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: path.Join(basePath, filepath.Dir(dst))}, + Ref: &provider.Reference{Path: filepath.Dir(dst)}, } parentStatResponse, err := client.Stat(ctx, parentStatReq) From 958f9b29555d197bb1b011cf62d2d6ba20ec3584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Tue, 26 Oct 2021 16:40:29 +0200 Subject: [PATCH 20/30] Fix expected failures for s3ng --- tests/acceptance/expected-failures-on-S3NG-storage.md | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index d51fb718bf..36cd2004c2 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -333,7 +333,6 @@ File and sync features in a shared scenario - [apiShareManagementBasicToShares/createShareToSharesFolder.feature:290](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L290) - [apiShareManagementBasicToShares/createShareToSharesFolder.feature:291](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L291) -- [apiShareManagementBasicToShares/deleteShareFromShares.feature:59](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/deleteShareFromShares.feature#L59) - [apiShareManagementToShares/mergeShare.feature:89](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/mergeShare.feature#L89) #### [Fix accepting/denying group shares](https://github.com/cs3org/reva/issues/1769) From 028b279997c758ed3f5bf7baf7c2d50b76201bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 27 Oct 2021 09:54:31 +0200 Subject: [PATCH 21/30] Be more robust when handling error cases --- internal/http/services/owncloud/ocdav/ocdav.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 10e07b67bd..c6f456e6e4 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -262,7 +262,7 @@ func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool } // If it's not a userid try if it is a user name - if userRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + if userRes.Status.Code != rpc.Code_CODE_OK { res, err := gatewayClient.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ Claim: "username", Value: requestUsernameOrID, @@ -275,7 +275,7 @@ func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool } // If still didn't find a user, fallback - if userRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + if userRes.Status.Code != rpc.Code_CODE_OK { userRes.User = &userpb.User{ Username: requestUsernameOrID, Id: &userpb.UserId{OpaqueId: requestUsernameOrID}, From 4f0949dd8a01dfe1e7f8590922207362ee2dfe73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 27 Oct 2021 09:55:35 +0200 Subject: [PATCH 22/30] WIP: Fix restoring recycle items --- internal/grpc/services/gateway/storageprovider.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 01babe3c91..30c40bf12f 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -1071,6 +1071,8 @@ func (s *svc) RestoreRecycleItem(ctx context.Context, req *provider.RestoreRecyc }, nil } + // TODO: HELP?!? + req.RestoreRef.Path = strings.TrimPrefix(req.RestoreRef.Path, destinationProviderInfo[0].ProviderPath) res, err := c.RestoreRecycleItem(ctx, req) if err != nil { return nil, errors.Wrap(err, "gateway: error calling RestoreRecycleItem") From a5ca0f31ee6d26ae549e4606d3cdf0594eb6e165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 27 Oct 2021 10:22:17 +0200 Subject: [PATCH 23/30] Fix unit tests --- .../sharing/shares/mocks/GatewayClient.go | 32 +++++++++++++++++++ .../apps/sharing/shares/shares_test.go | 20 +++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go index 68adad9cf1..566024d830 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -7,6 +7,8 @@ import ( collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + groupv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" grpc "google.golang.org/grpc" @@ -23,6 +25,36 @@ type GatewayClient struct { mock.Mock } +// Authenticate provides a mock function with given fields: ctx, in, opts +func (_m *GatewayClient) Authenticate(ctx context.Context, in *gatewayv1beta1.AuthenticateRequest, opts ...grpc.CallOption) (*gatewayv1beta1.AuthenticateResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *gatewayv1beta1.AuthenticateResponse + if rf, ok := ret.Get(0).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) *gatewayv1beta1.AuthenticateResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gatewayv1beta1.AuthenticateResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateShare provides a mock function with given fields: ctx, in, opts func (_m *GatewayClient) CreateShare(ctx context.Context, in *collaborationv1beta1.CreateShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.CreateShareResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go index b04ca576e7..78a5dc1c44 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_test.go @@ -26,6 +26,7 @@ import ( "net/url" "strings" + gateway "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" @@ -107,13 +108,22 @@ var _ = Describe("The ocs API", func() { }, }, nil) + user := &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: "admin", + }, + } + client.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userpb.GetUserByClaimResponse{ Status: status.NewOK(context.Background()), - User: &userpb.User{ - Id: &userpb.UserId{ - OpaqueId: "admin", - }, - }, + User: user, + }, nil) + client.On("GetUser", mock.Anything, mock.Anything).Return(&userpb.GetUserResponse{ + Status: status.NewOK(context.Background()), + User: user, + }, nil) + client.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{ + Status: status.NewOK(context.Background()), }, nil) client.On("GetShare", mock.Anything, mock.Anything).Return(&collaboration.GetShareResponse{ From df6021ec4f13fd367c6b47591d6e1e2fd7184038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 27 Oct 2021 10:38:55 +0200 Subject: [PATCH 24/30] Fix license headers --- .../apps/sharing/shares/mocks/GatewayClient.go | 18 ++++++++++++++++++ .../apps/sharing/shares/shares_suite_test.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go index 566024d830..d068b24597 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -1,3 +1,21 @@ +// 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. + // Code generated by mockery v1.0.0. DO NOT EDIT. package mocks diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go index 800177c0b0..1b681efb77 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_suite_test.go @@ -1,3 +1,21 @@ +// 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 shares_test import ( From a00d56220119c6b3b9317ce9cababeff39055bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 27 Oct 2021 11:11:21 +0200 Subject: [PATCH 25/30] Fix linter issues --- .../services/gateway/usershareprovider.go | 80 ------------------- .../apps/sharing/shares/pending_test.go | 10 --- .../handlers/apps/sharing/shares/shares.go | 12 ++- pkg/share/manager/json/json.go | 5 +- pkg/share/manager/sql/sql_test.go | 2 + pkg/user/manager/ldap/ldap.go | 9 ++- 6 files changed, 14 insertions(+), 104 deletions(-) diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index b6950d9c65..625971071b 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -20,7 +20,6 @@ package gateway import ( "context" - "fmt" "path" rtrace "github.com/cs3org/reva/pkg/trace" @@ -378,85 +377,6 @@ func (s *svc) removeReference(ctx context.Context, resourceID *provider.Resource return status.NewOK(ctx) } -func (s *svc) createReference(ctx context.Context, resourceID *provider.ResourceId) *rpc.Status { - ref := &provider.Reference{ - ResourceId: resourceID, - } - log := appctx.GetLogger(ctx) - - // get the metadata about the share - c, err := s.find(ctx, ref) - if err != nil { - if _, ok := err.(errtypes.IsNotFound); ok { - return status.NewNotFound(ctx, "storage provider not found") - } - return status.NewInternal(ctx, err, "error finding storage provider") - } - - statReq := &provider.StatRequest{ - Ref: ref, - } - - statRes, err := c.Stat(ctx, statReq) - if err != nil { - return status.NewInternal(ctx, err, "gateway: error calling Stat for the share resource id: "+resourceID.String()) - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - err := status.NewErrorFromCode(statRes.Status.GetCode(), "gateway") - log.Err(err).Msg("gateway: Stat failed on the share resource id: " + resourceID.String()) - return status.NewInternal(ctx, err, "error updating received share") - } - - homeRes, err := s.GetHome(ctx, &provider.GetHomeRequest{}) - if err != nil { - err := errors.Wrap(err, "gateway: error calling GetHome") - return status.NewInternal(ctx, err, "error updating received share") - } - - // reference path is the home path + some name - // CreateReferene(cs3://home/MyShares/x) - // that can end up in the storage provider like: - // /eos/user/.shadow/g/gonzalhu/MyShares/x - // A reference can point to any place, for that reason the namespace starts with cs3:// - // For example, a reference can point also to a dropbox resource: - // CreateReference(dropbox://x/y/z) - // It is the responsibility of the gateway to resolve these references and merge the response back - // from the main request. - // TODO(labkode): the name of the share should be the filename it points to by default. - refPath := path.Join(homeRes.Path, s.c.ShareFolder, path.Base(statRes.Info.Path)) - log.Info().Msg("mount path will be:" + refPath) - - createRefReq := &provider.CreateReferenceRequest{ - Ref: &provider.Reference{Path: refPath}, - // cs3 is the Scheme and %s/%s is the Opaque parts of a net.URL. - TargetUri: fmt.Sprintf("cs3:%s/%s", resourceID.GetStorageId(), resourceID.GetOpaqueId()), - } - - c, err = s.findByPath(ctx, homeRes.Path) - if err != nil { - if _, ok := err.(errtypes.IsNotFound); ok { - return status.NewNotFound(ctx, "storage provider not found") - } - return status.NewInternal(ctx, err, "error finding storage provider") - } - - createRefRes, err := c.CreateReference(ctx, createRefReq) - if err != nil { - log.Err(err).Msg("gateway: error calling GetHome") - return &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - } - } - - if createRefRes.Status.Code != rpc.Code_CODE_OK { - err := status.NewErrorFromCode(createRefRes.Status.GetCode(), "gateway") - return status.NewInternal(ctx, err, "error updating received share") - } - - return status.NewOK(ctx) -} - func (s *svc) denyGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee) (*rpc.Status, error) { ref := &provider.Reference{ ResourceId: id, diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go index 01a322e259..cd38f75f6e 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go @@ -53,16 +53,6 @@ var _ = Describe("The ocs API", func() { }) Describe("AcceptReceivedShare", func() { - type responseShare struct { - ID string `xml:"id"` - } - type listSharesData struct { - Shares []responseShare `xml:"element"` - } - type listSharesResponse struct { - Data listSharesData `xml:"data"` - } - var ( resID = &provider.ResourceId{ StorageId: "share1-storageid", diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index f7ce22c5cd..f95b4bfea4 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -280,7 +280,6 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "machine authentication failed", nil) return } - //granteeCtx = revactx.ContextSetToken(granteeCtx, authRes.Token) granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, revactx.TokenHeader, authRes.Token) lrs, ocsResponse := getSharesList(granteeCtx, client) @@ -370,12 +369,11 @@ func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri Message: err.Error(), Error: err, } - } else { - return nil, nil, &ocsError{ - Code: response.MetaBadRequest.StatusCode, - Message: err.Error(), - Error: err, - } + } + return nil, nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: err.Error(), + Error: err, } } role = conversions.RoleFromOCSPermissions(perm) diff --git a/pkg/share/manager/json/json.go b/pkg/share/manager/json/json.go index 27f94e2985..9347fa613b 100644 --- a/pkg/share/manager/json/json.go +++ b/pkg/share/manager/json/json.go @@ -403,10 +403,7 @@ func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.F // to the same resource. Leave the more explicit and hide the more explicit. In this case we hide the group shares // and return the user share to the user. filtered := make([]*collaboration.ReceivedShare, 0) - - for _, s := range rss { - filtered = append(filtered, s) - } + filtered = append(filtered, rss...) for i := range rss { for j := range rss { diff --git a/pkg/share/manager/sql/sql_test.go b/pkg/share/manager/sql/sql_test.go index 5837d847df..24d20ede45 100644 --- a/pkg/share/manager/sql/sql_test.go +++ b/pkg/share/manager/sql/sql_test.go @@ -299,6 +299,7 @@ var _ = Describe("SQL manager", func() { 31, // permissions, 0, // accepted ) + Expect(err).ToNot(HaveOccurred()) shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) Expect(err).ToNot(HaveOccurred()) @@ -436,6 +437,7 @@ var _ = Describe("SQL manager", func() { 31, // permissions, 0, // accepted ) + Expect(err).ToNot(HaveOccurred()) parentRef := &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ Id: &collaboration.ShareId{ OpaqueId: strconv.Itoa(parentID), diff --git a/pkg/user/manager/ldap/ldap.go b/pkg/user/manager/ldap/ldap.go index e87670dfe1..4f72e01896 100644 --- a/pkg/user/manager/ldap/ldap.go +++ b/pkg/user/manager/ldap/ldap.go @@ -149,7 +149,7 @@ func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User } defer l.Close() - userEntry, err := m.getLDAPUserById(ctx, l, uid) + userEntry, err := m.getLDAPUserByID(ctx, l, uid) if err != nil { return nil, err } @@ -350,11 +350,14 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri } defer l.Close() - userEntry, err := m.getLDAPUserById(ctx, l, uid) + userEntry, err := m.getLDAPUserByID(ctx, l, uid) + if err != nil { + return []string{}, err + } return m.getLDAPUserGroups(ctx, l, userEntry) } -func (m *manager) getLDAPUserById(ctx context.Context, conn *ldap.Conn, uid *userpb.UserId) (*ldap.Entry, error) { +func (m *manager) getLDAPUserByID(ctx context.Context, conn *ldap.Conn, uid *userpb.UserId) (*ldap.Entry, error) { log := appctx.GetLogger(ctx) // Search for the given clientID, use a sizeLimit of 1 to be able // to error out early when the userid is not unique From 38d8d72bb2b0b03bf8acc575bb752ab16977e81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 4 Nov 2021 10:13:56 +0000 Subject: [PATCH 26/30] add missing go.sum entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- go.mod | 1 + go.sum | 1 + 2 files changed, 2 insertions(+) diff --git a/go.mod b/go.mod index b73c551058..82870c392f 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/go-openapi/errors v0.20.1 // indirect github.com/go-openapi/strfmt v0.20.2 // indirect github.com/go-sql-driver/mysql v1.6.0 + github.com/gogo/protobuf v1.3.2 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/protobuf v1.5.2 github.com/gomodule/redigo v1.8.5 diff --git a/go.sum b/go.sum index 30e79e774b..d5f226ce40 100644 --- a/go.sum +++ b/go.sum @@ -276,6 +276,7 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= From e5bcf1ea0efad69efa1554575a98dba661129021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 4 Nov 2021 10:18:36 +0000 Subject: [PATCH 27/30] fix import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- internal/grpc/services/gateway/storageprovider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index ddda925a07..f8a9fbd918 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -33,7 +33,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" - "github.com/gogo/protobuf/types" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" @@ -722,7 +722,7 @@ func (s *svc) statAcrossProviders(ctx context.Context, req *provider.StatRequest Path: req.Ref.GetPath(), MimeType: "httpd/unix-directory", Size: 0, - Mtime: &types.Timestamp{}, + Mtime: &typesv1beta1.Timestamp{}, } for _, p := range providers { From 5b5f8ed2dd14d0ef375236b11f1c7319be91564a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 4 Nov 2021 12:39:46 +0000 Subject: [PATCH 28/30] add embedded mounts when listing /home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../grpc/services/gateway/storageprovider.go | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index f8a9fbd918..1d1bf20f6e 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -932,7 +932,30 @@ func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequ } if path.Clean(p) == s.getHome(ctx) { - return s.listHome(ctx, req) + res, err := s.listHome(ctx, req) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "listContainer ref: "+req.Ref.String(), err), + }, nil + } + + // Inject mountpoints if they do not exist on disk + embeddedMounts := s.findEmbeddedMounts(path.Clean(req.Ref.GetPath())) + for _, mount := range embeddedMounts { + for _, info := range res.Infos { + if info.Path == mount { // hmm this makes existing folders hide a mount, right? + continue + } + } + childStatRes, err := s.stat(ctx, &provider.StatRequest{Ref: &provider.Reference{Path: mount}}) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "ListContainer ref: "+req.Ref.String(), err), + }, nil + } + res.Infos = append(res.Infos, childStatRes.Info) + } + return res, nil } return s.listContainer(ctx, req) From 4e257e83faf639eb09d29c97bee2f2b78626ab09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 4 Nov 2021 13:17:39 +0000 Subject: [PATCH 29/30] remove unexpected passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- tests/acceptance/expected-failures-on-OCIS-storage.md | 2 -- tests/acceptance/expected-failures-on-S3NG-storage.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index aebba2e893..a1358b6870 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -328,8 +328,6 @@ File and sync features in a shared scenario - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:237](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L237) #### [Shares received in different ways are not merged](https://github.com/owncloud/ocis/issues/2711) -- [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:553](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L553) -- [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:554](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L554) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:598](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L598) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:599](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L599) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:621](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L621) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index 06dc63edf0..cec4fea7e9 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -330,8 +330,6 @@ File and sync features in a shared scenario - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:237](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L237) #### [Shares received in different ways are not merged](https://github.com/owncloud/ocis/issues/2711) -- [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:553](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L553) -- [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:554](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L554) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:598](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L598) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:599](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L599) - [apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature:621](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares1/createShareReceivedInMultipleWays.feature#L621) From d615470673ae1c413a5d61edda5aaaf33ed55a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 5 Nov 2021 14:00:35 +0000 Subject: [PATCH 30/30] add old logic as comment as we want to bring parts of it back for OCM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../grpc/services/gateway/storageprovider.go | 1081 +++++++++++++++++ .../services/gateway/webdavstorageprovider.go | 217 ++++ 2 files changed, 1298 insertions(+) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 1d1bf20f6e..5e03b9dffb 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -329,6 +329,159 @@ func (s *svc) InitiateFileDownload(ctx context.Context, req *provider.InitiateFi }, nil } return s.initiateFileDownload(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + if !s.inSharedFolder(ctx, p) { + statReq := &provider.StatRequest{Ref: req.Ref} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + if statRes.Status.Code != rpc.Code_CODE_OK { + return &gateway.InitiateFileDownloadResponse{ + Status: statRes.Status, + }, nil + } + return s.initiateFileDownload(ctx, req) + } + + if s.isSharedFolder(ctx, p) { + log.Debug().Str("path", p).Msg("path points to shared folder") + err := errtypes.PermissionDenied("gateway: cannot download share folder: path=" + p) + log.Err(err).Msg("gateway: error downloading") + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInvalidArg(ctx, "path points to share folder"), + }, nil + + } + + if s.isShareName(ctx, p) { + statReq := &provider.StatRequest{Ref: req.Ref} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + if statRes.Status.Code != rpc.Code_CODE_OK { + return &gateway.InitiateFileDownloadResponse{ + Status: statRes.Status, + }, nil + } + + if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { + err := errtypes.BadRequest(fmt.Sprintf("gateway: expected reference: got:%+v", statRes.Info)) + log.Err(err).Msg("gateway: error stating share name") + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error initiating download"), + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + // TODO(ishank011): pass this through the datagateway service + // For now, we just expose the file server to the user + ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), + }, nil + } + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileDownloadProtocol{ + { + Opaque: opaque, + Protocol: "simple", + DownloadEndpoint: ep, + }, + }, + }, nil + } + + // if it is a file allow download + if ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + log.Debug().Str("path", p).Interface("ri", ri).Msg("path points to share name file") + req.Ref.Path = ri.Path + log.Debug().Str("path", ri.Path).Msg("download") + return s.initiateFileDownload(ctx, req) + } + + log.Debug().Str("path", p).Interface("statRes", statRes).Msg("path:%s points to share name") + err = errtypes.PermissionDenied("gateway: cannot download share name: path=" + p) + log.Err(err).Str("path", p).Msg("gateway: error downloading") + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInvalidArg(ctx, "path points to share name"), + }, nil + } + + if s.isShareChild(ctx, p) { + log.Debug().Msgf("shared child: %s", p) + shareName, shareChild := s.splitShare(ctx, p) + + statReq := &provider.StatRequest{ + Ref: &provider.Reference{Path: shareName}, + } + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &gateway.InitiateFileDownloadResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + // TODO(ishank011): pass this through the datagateway service + // For now, we just expose the file server to the user + ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), + }, nil + } + return &gateway.InitiateFileDownloadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileDownloadProtocol{ + { + Opaque: opaque, + Protocol: "simple", + DownloadEndpoint: ep, + }, + }, + }, nil + } + + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + log.Debug().Str("path", req.Ref.Path).Msg("download") + return s.initiateFileDownload(ctx, req) + } + + panic("gateway: download: unknown path:" + p) + */ } func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { @@ -395,6 +548,146 @@ func (s *svc) InitiateFileUpload(ctx context.Context, req *provider.InitiateFile } return s.initiateFileUpload(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + return s.initiateFileUpload(ctx, req) + if !s.inSharedFolder(ctx, p) { + return s.initiateFileUpload(ctx, req) + } + + if s.isSharedFolder(ctx, p) { + log.Debug().Str("path", p).Msg("path points to shared folder") + err := errtypes.PermissionDenied("gateway: cannot upload to share folder: path=" + p) + log.Err(err).Msg("gateway: error downloading") + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInvalidArg(ctx, "path points to share folder"), + }, nil + + } + + if s.isShareName(ctx, p) { + log.Debug().Str("path", p).Msg("path points to share name") + statReq := &provider.StatRequest{Ref: req.Ref} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + if statRes.Status.Code != rpc.Code_CODE_OK { + return &gateway.InitiateFileUploadResponse{ + Status: statRes.Status, + }, nil + } + + if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { + err := errtypes.BadRequest(fmt.Sprintf("gateway: expected reference: got:%+v", statRes.Info)) + log.Err(err).Msg("gateway: error stating share name") + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error initiating upload"), + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + // TODO(ishank011): pass this through the datagateway service + // For now, we just expose the file server to the user + ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error downloading from webdav host: "+p), + }, nil + } + return &gateway.InitiateFileUploadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileUploadProtocol{ + { + Opaque: opaque, + Protocol: "simple", + UploadEndpoint: ep, + }, + }, + }, nil + } + + // if it is a file allow upload + if ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + log.Debug().Str("path", p).Interface("ri", ri).Msg("path points to share name file") + req.Ref.Path = ri.Path + log.Debug().Str("path", ri.Path).Msg("upload") + return s.initiateFileUpload(ctx, req) + } + + err = errtypes.PermissionDenied("gateway: cannot upload to share name: path=" + p) + log.Err(err).Msg("gateway: error uploading") + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInvalidArg(ctx, "path points to share name"), + }, nil + + } + + if s.isShareChild(ctx, p) { + log.Debug().Msgf("shared child: %s", p) + shareName, shareChild := s.splitShare(ctx, p) + + statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &gateway.InitiateFileUploadResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + // TODO(ishank011): pass this through the datagateway service + // For now, we just expose the file server to the user + ep, opaque, err := s.webdavRefTransferEndpoint(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &gateway.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "gateway: error uploading to webdav host: "+p), + }, nil + } + return &gateway.InitiateFileUploadResponse{ + Status: status.NewOK(ctx), + Protocols: []*gateway.FileUploadProtocol{ + { + Opaque: opaque, + Protocol: "simple", + UploadEndpoint: ep, + }, + }, + }, nil + } + + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + return s.initiateFileUpload(ctx, req) + } + + panic("gateway: upload: unknown path:" + p) + */ } func (s *svc) initiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { @@ -488,6 +781,69 @@ func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainer } return s.createContainer(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + if !s.inSharedFolder(ctx, p) { + return s.createContainer(ctx, req) + } + + if s.isSharedFolder(ctx, p) || s.isShareName(ctx, p) { + log.Debug().Msgf("path:%s points to shared folder or share name", p) + err := errtypes.PermissionDenied("gateway: cannot create container on share folder or share name: path=" + p) + log.Err(err).Msg("gateway: error creating container") + return &provider.CreateContainerResponse{ + Status: status.NewInvalidArg(ctx, "path points to share folder or share name"), + }, nil + + } + + if s.isShareChild(ctx, p) { + log.Debug().Msgf("shared child: %s", p) + shareName, shareChild := s.splitShare(ctx, p) + + statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &provider.CreateContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.CreateContainerResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &provider.CreateContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + err = s.webdavRefMkdir(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &provider.CreateContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error creating container on webdav host: "+p), + }, nil + } + return &provider.CreateContainerResponse{ + Status: status.NewOK(ctx), + }, nil + } + + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + return s.createContainer(ctx, req) + } + + panic("gateway: create container on unknown path:" + p) + */ } func (s *svc) createContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { @@ -515,6 +871,134 @@ func (s *svc) Delete(ctx context.Context, req *provider.DeleteRequest) (*provide } return s.delete(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + ctx, span := rtrace.Provider.Tracer("reva").Start(ctx, "Delete") + defer span.End() + + if !s.inSharedFolder(ctx, p) { + return s.delete(ctx, req) + } + + if s.isSharedFolder(ctx, p) { + // TODO(labkode): deleting share names should be allowed, means unmounting. + err := errtypes.BadRequest("gateway: cannot delete share folder or share name: path=" + p) + span.RecordError(err) + return &provider.DeleteResponse{ + Status: status.NewInvalidArg(ctx, "path points to share folder or share name"), + }, nil + + } + + if s.isShareName(ctx, p) { + log.Debug().Msgf("path:%s points to share name", p) + + sRes, err := s.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) + if err != nil { + return nil, err + } + + statRes, err := s.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Path: p, + }, + }) + if err != nil { + return nil, err + } + + // the following will check that: + // - the resource to delete is a share the current user received + // - signal the storage the delete must not land in the trashbin + // - delete the resource and update the share status to "rejected" + for _, share := range sRes.Shares { + if statRes != nil && (share.Share.ResourceId.OpaqueId == statRes.Info.Id.OpaqueId) && (share.Share.ResourceId.StorageId == statRes.Info.Id.StorageId) { + // this opaque needs explanation. It signals the storage the resource we're about to delete does not + // belong to the current user because it was share to her, thus delete the "node" and don't send it to + // the trash bin, since the share can be mounted as many times as desired. + req.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "deleting_shared_resource": { + Value: []byte("true"), + Decoder: "plain", + }, + }, + } + + // the following block takes care of updating the state of the share to "rejected". This will ensure the user + // can "Accept" the share once again. + // TODO should this be pending? If so, update the two comments above as well. If not, get rid of this comment. + share.State = collaboration.ShareState_SHARE_STATE_REJECTED + r := &collaboration.UpdateReceivedShareRequest{ + Share: share, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, + } + + _, err := s.UpdateReceivedShare(ctx, r) + if err != nil { + return nil, err + } + + return &provider.DeleteResponse{ + Status: status.NewOK(ctx), + }, nil + } + } + + return &provider.DeleteResponse{ + Status: status.NewNotFound(ctx, "could not find share"), + }, nil + } + + if s.isShareChild(ctx, p) { + shareName, shareChild := s.splitShare(ctx, p) + log.Debug().Msgf("path:%s sharename:%s sharechild: %s", p, shareName, shareChild) + + ref := &provider.Reference{Path: shareName} + + statReq := &provider.StatRequest{Ref: ref} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &provider.DeleteResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.DeleteResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &provider.DeleteResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + err = s.webdavRefDelete(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &provider.DeleteResponse{ + Status: status.NewInternal(ctx, err, "gateway: error deleting resource on webdav host: "+p), + }, nil + } + return &provider.DeleteResponse{ + Status: status.NewOK(ctx), + }, nil + } + + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + return s.delete(ctx, req) + } + + panic("gateway: delete called on unknown path:" + p) + */ } func (s *svc) delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { @@ -550,6 +1034,109 @@ func (s *svc) Move(ctx context.Context, req *provider.MoveRequest) (*provider.Mo } return s.move(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + if !s.inSharedFolder(ctx, p) && !s.inSharedFolder(ctx, dp) { + return s.move(ctx, req) + } + + // allow renaming the share folder, the mount point, not the target. + if s.isShareName(ctx, p) && s.isShareName(ctx, dp) { + log.Info().Msgf("gateway: move: renaming share mountpoint: from:%s to:%s", p, dp) + return s.move(ctx, req) + } + + // resolve references and check the ref points to the same base path, paranoia check. + if s.isShareChild(ctx, p) && s.isShareChild(ctx, dp) { + shareName, shareChild := s.splitShare(ctx, p) + dshareName, dshareChild := s.splitShare(ctx, dp) + log.Debug().Msgf("srcpath:%s dstpath:%s srcsharename:%s srcsharechild: %s dstsharename:%s dstsharechild:%s ", p, dp, shareName, shareChild, dshareName, dshareChild) + + srcStatReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} + srcStatRes, err := s.stat(ctx, srcStatReq) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+srcStatReq.Ref.String()), + }, nil + } + + if srcStatRes.Status.Code != rpc.Code_CODE_OK { + return &provider.MoveResponse{ + Status: srcStatRes.Status, + }, nil + } + + dstStatReq := &provider.StatRequest{Ref: &provider.Reference{Path: dshareName}} + dstStatRes, err := s.stat(ctx, dstStatReq) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+srcStatReq.Ref.String()), + }, nil + } + + if dstStatRes.Status.Code != rpc.Code_CODE_OK { + return &provider.MoveResponse{ + Status: srcStatRes.Status, + }, nil + } + + srcRi, srcProtocol, err := s.checkRef(ctx, srcStatRes.Info) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+srcStatRes.Info.Target, err), + }, nil + } + + if srcProtocol == "webdav" { + err = s.webdavRefMove(ctx, dstStatRes.Info.Target, shareChild, dshareChild) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "gateway: error moving resource on webdav host: "+p), + }, nil + } + return &provider.MoveResponse{ + Status: status.NewOK(ctx), + }, nil + } + dstRi, dstProtocol, err := s.checkRef(ctx, dstStatRes.Info) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+srcStatRes.Info.Target, err), + }, nil + } + + if dstProtocol == "webdav" { + err = s.webdavRefMove(ctx, dstStatRes.Info.Target, shareChild, dshareChild) + if err != nil { + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "gateway: error moving resource on webdav host: "+p), + }, nil + } + return &provider.MoveResponse{ + Status: status.NewOK(ctx), + }, nil + } + + src := &provider.Reference{ + Path: path.Join(srcRi.Path, shareChild), + } + dst := &provider.Reference{ + Path: path.Join(dstRi.Path, dshareChild), + } + + req.Source = src + req.Destination = dst + + return s.move(ctx, req) + } + + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "move", errtypes.BadRequest("gateway: move called on unknown path: "+p)), + }, nil + */ } func (s *svc) move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { @@ -789,7 +1376,210 @@ func (s *svc) Stat(ctx context.Context, req *provider.StatRequest) (*provider.St } return s.stat(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + if s.isSharedFolder(ctx, p) { + return s.statSharesFolder(ctx) + } + + if !s.inSharedFolder(ctx, p) { + if res != nil { + return res, nil + } + return s.stat(ctx, req) + } + + // we need to provide the info of the target, not the reference. + if s.isShareName(ctx, p) { + // If we haven't returned an error by now and res is nil, it means that + // req is an absolute path based ref, so we didn't stat it previously. + // So stat it now + if res == nil { + res, err = s.stat(ctx, req) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+req.Ref.String()), + }, nil + } + + if res.Status.Code != rpc.Code_CODE_OK { + return &provider.StatResponse{ + Status: res.Status, + }, nil + } + } + + ri, protocol, err := s.checkRef(ctx, res.Info) + if err != nil { + return &provider.StatResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+res.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + ri, err = s.webdavRefStat(ctx, res.Info.Target) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "gateway: error resolving webdav reference: "+p), + }, nil + } + } + + // we need to make sure we don't expose the reference target in the resource + // information. For example, if requests comes to: /home/MyShares/photos and photos + // is reference to /user/peter/Holidays/photos, we need to still return to the user + // /home/MyShares/photos + orgPath := res.Info.Path + res.Info = ri + res.Info.Path = orgPath + return res, nil + + } + + if s.isShareChild(ctx, p) { + shareName, shareChild := s.splitShare(ctx, p) + + statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.StatResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &provider.StatResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + ri, err = s.webdavRefStat(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "gateway: error resolving webdav reference: "+p), + }, nil + } + ri.Path = p + return &provider.StatResponse{ + Status: status.NewOK(ctx), + Info: ri, + }, nil + } + + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + res, err := s.stat(ctx, req) + if err != nil { + return &provider.StatResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+req.Ref.String()), + }, nil + } + if res.Status.Code != rpc.Code_CODE_OK { + return &provider.StatResponse{ + Status: res.Status, + }, nil + } + + // we need to make sure we don't expose the reference target in the resource + // information. + res.Info.Path = p + return res, nil + } + + panic("gateway: stating an unknown path:" + p) + */ +} + +// The old logic had to check if a path pointed to the share folder a share mount point or a share child +// It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the +// old logic readable. +/* +func (s *svc) checkRef(ctx context.Context, ri *provider.ResourceInfo) (*provider.ResourceInfo, string, error) { + if ri.Type != provider.ResourceType_RESOURCE_TYPE_REFERENCE { + panic("gateway: calling checkRef on a non reference type:" + ri.String()) + } + + // reference types MUST have a target resource id. + if ri.Target == "" { + err := errtypes.BadRequest("gateway: ref target is an empty uri") + return nil, "", err + } + + uri, err := url.Parse(ri.Target) + if err != nil { + return nil, "", errors.Wrapf(err, "gateway: error parsing target uri: %s", ri.Target) + } + + switch uri.Scheme { + case "cs3": + ref, err := s.handleCS3Ref(ctx, uri.Opaque) + return ref, "cs3", err + case "webdav": + return nil, "webdav", nil + default: + err := errtypes.BadRequest("gateway: no reference handler for scheme: " + uri.Scheme) + return nil, "", err + } +} + +func (s *svc) handleCS3Ref(ctx context.Context, opaque string) (*provider.ResourceInfo, error) { + // a cs3 ref has the following layout: / + parts := strings.SplitN(opaque, "/", 2) + if len(parts) < 2 { + err := errtypes.BadRequest("gateway: cs3 ref does not follow the layout storageid/opaqueid:" + opaque) + return nil, err + } + + // we could call here the Stat method again, but that is calling for problems in case + // there is a loop of targets pointing to targets, so better avoid it. + + req := &provider.StatRequest{ + Ref: &provider.Reference{ + ResourceId: &provider.ResourceId{ + StorageId: parts[0], + OpaqueId: parts[1], + }, + }, + } + res, err := s.stat(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling stat") + } + + if res.Status.Code != rpc.Code_CODE_OK { + switch res.Status.Code { + case rpc.Code_CODE_NOT_FOUND: + return nil, errtypes.NotFound(req.Ref.String()) + case rpc.Code_CODE_PERMISSION_DENIED: + return nil, errtypes.PermissionDenied(req.Ref.String()) + case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE: + return nil, errtypes.BadRequest(req.Ref.String()) + case rpc.Code_CODE_UNIMPLEMENTED: + return nil, errtypes.NotSupported(req.Ref.String()) + default: + return nil, errtypes.InternalError("gateway: error stating target reference") + } + } + + if res.Info.Type == provider.ResourceType_RESOURCE_TYPE_REFERENCE { + err := errtypes.BadRequest("gateway: error the target of a reference cannot be another reference") + return nil, err + } + + return res.Info, nil } +*/ func (s *svc) ListContainerStream(_ *provider.ListContainerStreamRequest, _ gateway.GatewayAPI_ListContainerStreamServer) error { return errtypes.NotSupported("Unimplemented") @@ -814,6 +1604,51 @@ func (s *svc) listHome(ctx context.Context, req *provider.ListContainerRequest) return lcr, nil } +// The old logic had to check if a path pointed to the share folder a share mount point or a share child +// It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the +// old logic readable. +/* +func (s *svc) listSharesFolder(ctx context.Context) (*provider.ListContainerResponse, error) { + lcr, err := s.listContainer(ctx, &provider.ListContainerRequest{Ref: &provider.Reference{Path: s.getSharedFolder(ctx)}}) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error listing shared folder"), + }, nil + } + if lcr.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: lcr.Status, + }, nil + } + checkedInfos := make([]*provider.ResourceInfo, 0) + for i := range lcr.Infos { + info, protocol, err := s.checkRef(ctx, lcr.Infos[i]) + if err != nil { + // create status to log the proper messages + // this might arise when the shared resource has been moved to the recycle bin + // this might arise when the resource was unshared, but the share reference was not removed + status.NewStatusFromErrType(ctx, "error resolving reference "+lcr.Infos[i].Target, err) + // continue on errors so the user can see a list of the working shares + continue + } + + if protocol == "webdav" { + info, err = s.webdavRefStat(ctx, lcr.Infos[i].Target) + if err != nil { + // Might be the case that the webdav token has expired + continue + } + } + + info.Path = lcr.Infos[i].Path + checkedInfos = append(checkedInfos, info) + } + lcr.Infos = checkedInfos + + return lcr, nil +} +*/ + func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { providers, err := s.findProviders(ctx, req.Ref) if err != nil { @@ -959,6 +1794,178 @@ func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequ } return s.listContainer(ctx, req) + + // The old logic had to check if a path pointed to the share folder a share mount point or a share child + // It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the + // old logic readable. + /* + if path.Clean(p) == s.getHome(ctx) { + return s.listHome(ctx, req) + } + + if s.isSharedFolder(ctx, p) { + return s.listSharesFolder(ctx) + } + + if !s.inSharedFolder(ctx, p) { + return s.listContainer(ctx, req) + } + + // we need to provide the info of the target, not the reference. + if s.isShareName(ctx, p) { + statReq := &provider.StatRequest{Ref: &provider.Reference{Path: p}} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating share:"+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + infos, err := s.webdavRefLs(ctx, statRes.Info.Target) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error listing webdav reference: "+p), + }, nil + } + + for _, info := range infos { + base := path.Base(info.Path) + info.Path = path.Join(p, base) + } + return &provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: infos, + }, nil + } + + if ri.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { + err := errtypes.NotSupported("gateway: list container: cannot list non-container type:" + ri.Path) + log.Err(err).Msg("gateway: error listing") + return &provider.ListContainerResponse{ + Status: status.NewInvalidArg(ctx, "resource is not a container"), + }, nil + } + + newReq := &provider.ListContainerRequest{ + Ref: &provider.Reference{Path: ri.Path}, + ArbitraryMetadataKeys: req.ArbitraryMetadataKeys, + } + newRes, err := s.listContainer(ctx, newReq) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error listing "+newReq.Ref.String()), + }, nil + } + + if newRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: newRes.Status, + }, nil + } + + // paths needs to be converted + for _, info := range newRes.Infos { + base := path.Base(info.Path) + info.Path = path.Join(p, base) + } + + return newRes, nil + + } + + if s.isShareChild(ctx, p) { + shareName, shareChild := s.splitShare(ctx, p) + + statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} + statRes, err := s.stat(ctx, statReq) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error stating share child "+statReq.Ref.String()), + }, nil + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: statRes.Status, + }, nil + } + + ri, protocol, err := s.checkRef(ctx, statRes.Info) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+statRes.Info.Target, err), + }, nil + } + + if protocol == "webdav" { + infos, err := s.webdavRefLs(ctx, statRes.Info.Target, shareChild) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error listing webdav reference: "+p), + }, nil + } + + for _, info := range infos { + base := path.Base(info.Path) + info.Path = path.Join(shareName, shareChild, base) + } + return &provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: infos, + }, nil + } + + if ri.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { + err := errtypes.NotSupported("gateway: list container: cannot list non-container type:" + ri.Path) + log.Err(err).Msg("gateway: error listing") + return &provider.ListContainerResponse{ + Status: status.NewInvalidArg(ctx, "resource is not a container"), + }, nil + } + + newReq := &provider.ListContainerRequest{ + Ref: &provider.Reference{Path: path.Join(ri.Path, shareChild)}, + ArbitraryMetadataKeys: req.ArbitraryMetadataKeys, + } + newRes, err := s.listContainer(ctx, newReq) + if err != nil { + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, err, "gateway: error listing "+newReq.Ref.String()), + }, nil + } + + if newRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: newRes.Status, + }, nil + } + + // paths needs to be converted + for _, info := range newRes.Infos { + base := path.Base(info.Path) + info.Path = path.Join(shareName, shareChild, base) + } + + return newRes, nil + + } + + panic("gateway: stating an unknown path:" + p) + */ } func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...string) (string, *rpc.Status) { @@ -983,6 +1990,80 @@ func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...stri return "", &rpc.Status{Code: rpc.Code_CODE_INTERNAL} } +// The old logic had to check if a path pointed to the share folder a share mount point or a share child +// It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the +// old logic readable. +/* +// /home/MyShares/ +func (s *svc) isSharedFolder(ctx context.Context, p string) bool { + return s.split(ctx, p, 2) +} + +// /home/MyShares/photos/ +func (s *svc) isShareName(ctx context.Context, p string) bool { + return s.split(ctx, p, 3) +} + +// /home/MyShares/photos/Ibiza/beach.png +func (s *svc) isShareChild(ctx context.Context, p string) bool { + return s.split(ctx, p, 4) +} + +// always validate that the path contains the share folder +// split cannot be called with i<2 +func (s *svc) split(ctx context.Context, p string, i int) bool { + log := appctx.GetLogger(ctx) + if i < 2 { + panic("split called with i < 2") + } + + parts := s.splitPath(ctx, p) + + // validate that we have always at least two elements + if len(parts) < 2 { + return false + } + + // validate the share folder is always the second element, first element is always the hardcoded value of "home" + if parts[1] != s.c.ShareFolder { + log.Debug().Msgf("gateway: split: parts[1]:%+v != shareFolder:%+v", parts[1], s.c.ShareFolder) + return false + } + + log.Debug().Msgf("gateway: split: path:%+v parts:%+v shareFolder:%+v", p, parts, s.c.ShareFolder) + + if len(parts) == i && parts[i-1] != "" { + return true + } + + return false +} + +// path must contain a share path with share children, if not it will panic. +// should be called after checking isShareChild == true +func (s *svc) splitShare(ctx context.Context, p string) (string, string) { + parts := s.splitPath(ctx, p) + if len(parts) != 4 { + panic("gateway: path for splitShare does not contain 4 elements:" + p) + } + + shareName := path.Join("/", parts[0], parts[1], parts[2]) + shareChild := path.Join("/", parts[3]) + return shareName, shareChild +} + +func (s *svc) splitPath(_ context.Context, p string) []string { + p = strings.Trim(p, "/") + return strings.SplitN(p, "/", 4) // ["home", "MyShares", "photos", "Ibiza/beach.png"] +} + +func (s *svc) getSharedFolder(ctx context.Context) string { + home := s.getHome(ctx) + shareFolder := path.Join(home, s.c.ShareFolder) + return shareFolder +} +*/ + func (s *svc) CreateSymlink(ctx context.Context, req *provider.CreateSymlinkRequest) (*provider.CreateSymlinkResponse, error) { return &provider.CreateSymlinkResponse{ Status: status.NewUnimplemented(ctx, errtypes.NotSupported("CreateSymlink not implemented"), "CreateSymlink not implemented"), diff --git a/internal/grpc/services/gateway/webdavstorageprovider.go b/internal/grpc/services/gateway/webdavstorageprovider.go index 10782dba91..528c95bd3b 100644 --- a/internal/grpc/services/gateway/webdavstorageprovider.go +++ b/internal/grpc/services/gateway/webdavstorageprovider.go @@ -33,6 +33,182 @@ type webdavEndpoint struct { token string } +// The old logic had to check if a path pointed to the share folder a share mount point or a share child +// It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the +// old logic readable. +/* +func (s *svc) webdavRefStat(ctx context.Context, targetURL string, nameQueries ...string) (*provider.ResourceInfo, error) { + targetURL, err := appendNameQuery(targetURL, nameQueries...) + if err != nil { + return nil, err + } + + ep, err := s.extractEndpointInfo(ctx, targetURL) + if err != nil { + return nil, err + } + webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) + if err != nil { + return nil, err + } + + c := gowebdav.NewClient(webdavEP, "", "") + c.SetHeader(ctxpkg.TokenHeader, ep.token) + + // TODO(ishank011): We need to call PROPFIND ourselves as we need to retrieve + // ownloud-specific fields to get the resource ID and permissions. + info, err := c.Stat(ep.filePath) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("gateway: error statting %s at the webdav endpoint: %s", ep.filePath, webdavEP)) + } + return normalize(info.(*gowebdav.File)), nil +} + +func (s *svc) webdavRefLs(ctx context.Context, targetURL string, nameQueries ...string) ([]*provider.ResourceInfo, error) { + targetURL, err := appendNameQuery(targetURL, nameQueries...) + if err != nil { + return nil, err + } + + ep, err := s.extractEndpointInfo(ctx, targetURL) + if err != nil { + return nil, err + } + webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) + if err != nil { + return nil, err + } + + c := gowebdav.NewClient(webdavEP, "", "") + c.SetHeader(ctxpkg.TokenHeader, ep.token) + + // TODO(ishank011): We need to call PROPFIND ourselves as we need to retrieve + // ownloud-specific fields to get the resource ID and permissions. + infos, err := c.ReadDir(ep.filePath) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("gateway: error listing %s at the webdav endpoint: %s", ep.filePath, webdavEP)) + } + + mds := []*provider.ResourceInfo{} + for _, fi := range infos { + info := fi.(gowebdav.File) + mds = append(mds, normalize(&info)) + } + return mds, nil +} + +func (s *svc) webdavRefMkdir(ctx context.Context, targetURL string, nameQueries ...string) error { + targetURL, err := appendNameQuery(targetURL, nameQueries...) + if err != nil { + return err + } + + ep, err := s.extractEndpointInfo(ctx, targetURL) + if err != nil { + return err + } + webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) + if err != nil { + return err + } + + c := gowebdav.NewClient(webdavEP, "", "") + c.SetHeader(ctxpkg.TokenHeader, ep.token) + + err = c.Mkdir(ep.filePath, 0700) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("gateway: error creating dir %s at the webdav endpoint: %s", ep.filePath, webdavEP)) + } + return nil +} + +func (s *svc) webdavRefMove(ctx context.Context, targetURL, src, destination string) error { + srcURL, err := appendNameQuery(targetURL, src) + if err != nil { + return err + } + srcEP, err := s.extractEndpointInfo(ctx, srcURL) + if err != nil { + return err + } + srcWebdavEP, err := s.getWebdavEndpoint(ctx, srcEP.endpoint) + if err != nil { + return err + } + + destURL, err := appendNameQuery(targetURL, destination) + if err != nil { + return err + } + destEP, err := s.extractEndpointInfo(ctx, destURL) + if err != nil { + return err + } + + c := gowebdav.NewClient(srcWebdavEP, "", "") + c.SetHeader(ctxpkg.TokenHeader, srcEP.token) + + err = c.Rename(srcEP.filePath, destEP.filePath, true) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("gateway: error renaming %s to %s at the webdav endpoint: %s", srcEP.filePath, destEP.filePath, srcWebdavEP)) + } + return nil +} + +func (s *svc) webdavRefDelete(ctx context.Context, targetURL string, nameQueries ...string) error { + targetURL, err := appendNameQuery(targetURL, nameQueries...) + if err != nil { + return err + } + + ep, err := s.extractEndpointInfo(ctx, targetURL) + if err != nil { + return err + } + webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) + if err != nil { + return err + } + + c := gowebdav.NewClient(webdavEP, "", "") + c.SetHeader(ctxpkg.TokenHeader, ep.token) + + err = c.Remove(ep.filePath) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("gateway: error removing %s at the webdav endpoint: %s", ep.filePath, webdavEP)) + } + return nil +} + +func (s *svc) webdavRefTransferEndpoint(ctx context.Context, targetURL string, nameQueries ...string) (string, *types.Opaque, error) { + targetURL, err := appendNameQuery(targetURL, nameQueries...) + if err != nil { + return "", nil, err + } + + ep, err := s.extractEndpointInfo(ctx, targetURL) + if err != nil { + return "", nil, err + } + webdavEP, err := s.getWebdavEndpoint(ctx, ep.endpoint) + if err != nil { + return "", nil, err + } + + return webdavEP, &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "webdav-file-path": { + Decoder: "plain", + Value: []byte(ep.filePath), + }, + "webdav-token": { + Decoder: "plain", + Value: []byte(ep.token), + }, + }, + }, nil +} +*/ func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) { if targetURL == "" { return nil, errtypes.BadRequest("gateway: ref target is an empty uri") @@ -58,6 +234,47 @@ func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webda }, nil } +// The old logic had to check if a path pointed to the share folder a share mount point or a share child +// It also dealt with webdav references for OCM shares. The code below is commented en bloc to keep the +// old logic readable. +/* +func (s *svc) getWebdavEndpoint(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.Endpoint.Path, nil + } + } + return "", errtypes.NotFound(domain) +} + +func normalize(info *gowebdav.File) *provider.ResourceInfo { + return &provider.ResourceInfo{ + // TODO(ishank011): Add Id, PermissionSet, Owner + Path: info.Path(), + Type: getResourceType(info.IsDir()), + Etag: info.ETag(), + MimeType: info.ContentType(), + Size: uint64(info.Size()), + Mtime: &types.Timestamp{ + Seconds: uint64(info.ModTime().Unix()), + }, + } +} + +func getResourceType(isDir bool) provider.ResourceType { + if isDir { + return provider.ResourceType_RESOURCE_TYPE_CONTAINER + } + return provider.ResourceType_RESOURCE_TYPE_FILE +} +*/ + func appendNameQuery(targetURL string, nameQueries ...string) (string, error) { uri, err := url.Parse(targetURL) if err != nil {