diff --git a/.drone.star b/.drone.star index 101f46a0da..cdb9632f50 100644 --- a/.drone.star +++ b/.drone.star @@ -746,7 +746,9 @@ 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 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(), @@ -821,7 +823,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/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 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= 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 5292ac309a..5e03b9dffb 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -20,28 +20,25 @@ package gateway import ( "context" + "crypto/md5" "fmt" + "io" "net/url" "path" "strings" "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" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "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" @@ -308,166 +305,183 @@ 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) + 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 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") + if statRes.Status.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileDownloadResponse{ - Status: status.NewInvalidArg(ctx, "path points to share folder"), + Status: statRes.Status, }, nil - } + return s.initiateFileDownload(ctx, req) - 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 + // 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 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") + 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.NewInternal(ctx, err, "gateway: error initiating download"), + Status: status.NewInvalidArg(ctx, "path points to share folder"), }, 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 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 downloading from webdav host: "+p), + 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 &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) - } + 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 + } - 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 - } + 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 s.isShareChild(ctx, p) { - log.Debug().Msgf("shared child: %s", p) - shareName, shareChild := s.splitShare(ctx, p) + 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 + } - 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 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) + } - if statRes.Status.Code != rpc.Code_CODE_OK { + 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: statRes.Status, + Status: status.NewInvalidArg(ctx, "path points to share name"), }, 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 s.isShareChild(ctx, p) { + log.Debug().Msgf("shared child: %s", p) + shareName, shareChild := s.splitShare(ctx, p) - 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) + 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 downloading from webdav host: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, nil } - return &gateway.InitiateFileDownloadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileDownloadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - DownloadEndpoint: ep, + + 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 - } + }, 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) - } + // 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) + panic("gateway: download: unknown path:" + p) + */ } func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { @@ -523,150 +537,157 @@ 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 - - } + return s.initiateFileUpload(ctx, req) - 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 + // 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 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") + 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.NewInternal(ctx, err, "gateway: error initiating upload"), + Status: status.NewInvalidArg(ctx, "path points to share folder"), }, 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 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 downloading from webdav host: "+p), + 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 } - 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) - } + 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 + } - 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 + 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 s.isShareChild(ctx, p) { - log.Debug().Msgf("shared child: %s", p) - shareName, shareChild := s.splitShare(ctx, p) + // 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) + } - statReq := &provider.StatRequest{Ref: &provider.Reference{Path: shareName}} - statRes, err := s.stat(ctx, statReq) - if err != nil { + err = errtypes.PermissionDenied("gateway: cannot upload to share name: path=" + p) + log.Err(err).Msg("gateway: error uploading") return &gateway.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), + Status: status.NewInvalidArg(ctx, "path points to share name"), }, 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 s.isShareChild(ctx, p) { + log.Debug().Msgf("shared child: %s", p) + shareName, shareChild := s.splitShare(ctx, p) - 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) + 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 uploading to webdav host: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, nil } - return &gateway.InitiateFileUploadResponse{ - Status: status.NewOK(ctx), - Protocols: []*gateway.FileUploadProtocol{ - { - Opaque: opaque, - Protocol: "simple", - UploadEndpoint: ep, + + 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 - } + }, nil + } - // append child to target - req.Ref.Path = path.Join(ri.Path, shareChild) - return s.initiateFileUpload(ctx, req) - } + // append child to target + req.Ref.Path = path.Join(ri.Path, shareChild) + return s.initiateFileUpload(ctx, req) + } - panic("gateway: upload: unknown path:" + p) + panic("gateway: upload: unknown path:" + p) + */ } func (s *svc) initiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { @@ -748,76 +769,81 @@ 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 + return s.createContainer(ctx, req) - } - - 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 + // 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 statRes.Status.Code != rpc.Code_CODE_OK { + 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: statRes.Status, + Status: status.NewInvalidArg(ctx, "path points to share folder or share name"), }, 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 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 creating container on webdav host: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, 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) - } + 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 + } - panic("gateway: create container on unknown path:" + p) + // 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) { @@ -836,143 +862,143 @@ 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 + 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.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 + if !s.inSharedFolder(ctx, p) { + return s.delete(ctx, req) } - 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", - }, - }, - } + 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 - // 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 - } + if s.isShareName(ctx, p) { + log.Debug().Msgf("path:%s points to share name", p) - return &provider.DeleteResponse{ - Status: status.NewOK(ctx), - }, nil + sRes, err := s.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) + if err != nil { + return nil, err } - } - 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) + statRes, err := s.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Path: p, + }, + }) + if err != nil { + return nil, err + } - ref := &provider.Reference{Path: shareName} + // 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 + } + } - 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()), + Status: status.NewNotFound(ctx, "could not find share"), }, nil } - if statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.DeleteResponse{ - Status: statRes.Status, - }, nil - } + if s.isShareChild(ctx, p) { + shareName, shareChild := s.splitShare(ctx, p) + log.Debug().Msgf("path:%s sharename:%s sharechild: %s", p, shareName, shareChild) - 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 - } + ref := &provider.Reference{Path: shareName} - if protocol == "webdav" { - err = s.webdavRefDelete(ctx, statRes.Info.Target, shareChild) + statReq := &provider.StatRequest{Ref: ref} + statRes, err := s.stat(ctx, statReq) if err != nil { return &provider.DeleteResponse{ - Status: status.NewInternal(ctx, err, "gateway: error deleting resource on webdav host: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, 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) - } + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.DeleteResponse{ + Status: statRes.Status, + }, nil + } - panic("gateway: delete called on unknown path:" + p) + 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) { @@ -993,118 +1019,124 @@ 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) - } - - // 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) - } + 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 + // 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) } - if srcStatRes.Status.Code != rpc.Code_CODE_OK { - return &provider.MoveResponse{ - Status: srcStatRes.Status, - }, nil + // 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) } - 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 - } + // 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) - if dstStatRes.Status.Code != rpc.Code_CODE_OK { - return &provider.MoveResponse{ - Status: srcStatRes.Status, - }, nil - } + 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 + } - 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 srcStatRes.Status.Code != rpc.Code_CODE_OK { + return &provider.MoveResponse{ + Status: srcStatRes.Status, + }, nil + } - if srcProtocol == "webdav" { - err = s.webdavRefMove(ctx, dstStatRes.Info.Target, shareChild, dshareChild) + 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 moving resource on webdav host: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+srcStatReq.Ref.String()), }, 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 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.NewInternal(ctx, err, "gateway: error moving resource on webdav host: "+p), + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+srcStatRes.Info.Target, err), }, 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), - } + 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 + } - req.Source = src - req.Destination = dst + src := &provider.Reference{ + Path: path.Join(srcRi.Path, shareChild), + } + dst := &provider.Reference{ + Path: path.Join(dstRi.Path, dshareChild), + } - return s.move(ctx, req) - } + req.Source = src + req.Destination = dst - return &provider.MoveResponse{ - Status: status.NewStatusFromErrType(ctx, "move", errtypes.BadRequest("gateway: move called on unknown path: "+p)), - }, nil + 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) { @@ -1198,23 +1230,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) @@ -1222,59 +1237,13 @@ 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()}) - } + } else 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 - } - - 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 -} - func (s *svc) stat(ctx context.Context, req *provider.StatRequest) (*provider.StatResponse, error) { providers, err := s.findProviders(ctx, req.Ref) if err != nil { @@ -1292,11 +1261,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) @@ -1314,7 +1309,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 { @@ -1380,125 +1375,136 @@ 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) - } + return s.stat(ctx, req) - if !s.inSharedFolder(ctx, p) { - if res != nil { - return res, 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. + /* + if s.isSharedFolder(ctx, p) { + return s.statSharesFolder(ctx) } - 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 !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.NewInternal(ctx, err, "gateway: error stating ref:"+req.Ref.String()), + Status: status.NewStatusFromErrType(ctx, "error resolving reference "+res.Info.Target, err), }, nil } - if res.Status.Code != rpc.Code_CODE_OK { - return &provider.StatResponse{ - Status: res.Status, - }, 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 + } } - } - 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 + // 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 protocol == "webdav" { - ri, err = s.webdavRefStat(ctx, res.Info.Target) + 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 resolving webdav reference: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating ref:"+statReq.Ref.String()), }, 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 + } - 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 + } - 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 + } - if protocol == "webdav" { - ri, err = s.webdavRefStat(ctx, statRes.Info.Target, shareChild) + // 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 resolving webdav reference: "+p), + 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.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 } - // 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) + 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()) @@ -1573,6 +1579,7 @@ func (s *svc) handleCS3Ref(ctx context.Context, opaque string) (*provider.Resour return res.Info, nil } +*/ func (s *svc) ListContainerStream(_ *provider.ListContainerStreamRequest, _ gateway.GatewayAPI_ListContainerStreamServer) error { return errtypes.NotSupported("Unimplemented") @@ -1594,27 +1601,13 @@ 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 } +// 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 { @@ -1654,6 +1647,7 @@ func (s *svc) listSharesFolder(ctx context.Context) (*provider.ListContainerResp return lcr, nil } +*/ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { providers, err := s.findProviders(ctx, req.Ref) @@ -1733,6 +1727,27 @@ func (s *svc) listContainerAcrossProviders(ctx context.Context, req *provider.Li infos = append(infos, info) } + // 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, @@ -1740,8 +1755,6 @@ func (s *svc) listContainerAcrossProviders(ctx context.Context, req *provider.Li } 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) } @@ -1754,171 +1767,205 @@ 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 + } - if s.isSharedFolder(ctx, p) { - return s.listSharesFolder(ctx) + // 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 } - if !s.inSharedFolder(ctx, p) { - return s.listContainer(ctx, req) - } + 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 + // 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 statRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: statRes.Status, - }, nil + if s.isSharedFolder(ctx, p) { + return s.listSharesFolder(ctx) } - 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 !s.inSharedFolder(ctx, p) { + return s.listContainer(ctx, req) } - if protocol == "webdav" { - infos, err := s.webdavRefLs(ctx, statRes.Info.Target) + // 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 listing webdav reference: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating share:"+statReq.Ref.String()), }, nil } - for _, info := range infos { - base := path.Base(info.Path) - info.Path = path.Join(p, base) + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: statRes.Status, + }, nil } - 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 - } + 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 + } - 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 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 + } - if newRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: newRes.Status, - }, 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 + } - // paths needs to be converted - for _, info := range newRes.Infos { - base := path.Base(info.Path) - info.Path = path.Join(p, base) - } + 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 + } - return newRes, 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 + } - if s.isShareChild(ctx, p) { - shareName, shareChild := s.splitShare(ctx, p) + // paths needs to be converted + for _, info := range newRes.Infos { + base := path.Base(info.Path) + info.Path = path.Join(p, base) + } - 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 - } + return newRes, 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 s.isShareChild(ctx, p) { + shareName, shareChild := s.splitShare(ctx, p) - if protocol == "webdav" { - infos, err := s.webdavRefLs(ctx, statRes.Info.Target, shareChild) + 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 listing webdav reference: "+p), + Status: status.NewInternal(ctx, err, "gateway: error stating share child "+statReq.Ref.String()), }, nil } - for _, info := range infos { - base := path.Base(info.Path) - info.Path = path.Join(shareName, shareChild, base) + if statRes.Status.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: statRes.Status, + }, nil } - 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 - } + 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 + } - 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 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 + } - if newRes.Status.Code != rpc.Code_CODE_OK { - return &provider.ListContainerResponse{ - Status: newRes.Status, - }, 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 + } - // paths needs to be converted - for _, info := range newRes.Infos { - base := path.Base(info.Path) - info.Path = path.Join(shareName, shareChild, base) - } + 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 + } - return newRes, 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) + panic("gateway: stating an unknown path:" + p) + */ } func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...string) (string, *rpc.Status) { @@ -1943,6 +1990,10 @@ 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) @@ -2011,6 +2062,7 @@ func (s *svc) getSharedFolder(ctx context.Context) string { 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{ @@ -2072,6 +2124,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{ @@ -2079,6 +2150,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") @@ -2143,6 +2216,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..625971071b 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -20,9 +20,10 @@ package gateway import ( "context" - "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 +38,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 +271,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 +303,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 { @@ -436,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, refPath) - 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/grpc/services/gateway/webdavstorageprovider.go b/internal/grpc/services/gateway/webdavstorageprovider.go index 8d98e8688c..528c95bd3b 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,6 +33,10 @@ 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 { @@ -211,7 +208,7 @@ func (s *svc) webdavRefTransferEndpoint(ctx context.Context, targetURL string, n }, }, 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,6 +234,10 @@ 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, @@ -272,6 +273,7 @@ func getResourceType(isDir bool) provider.ResourceType { } return provider.ResourceType_RESOURCE_TYPE_FILE } +*/ func appendNameQuery(targetURL string, nameQueries ...string) (string, error) { uri, err := url.Parse(targetURL) 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..c6f456e6e4 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" @@ -99,6 +100,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"` @@ -235,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 @@ -243,12 +245,47 @@ 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 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: requestUsernameOrID}, + }) + if err != nil { + return "", "", err + } + + // If it's not a userid try if it is a user name + if userRes.Status.Code != rpc.Code_CODE_OK { + 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_OK { + userRes.User = &userpb.User{ + Username: requestUsernameOrID, + Id: &userpb.UserId{OpaqueId: requestUsernameOrID}, + } + } + + 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/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) diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index 246a7a8e01..abd984b9ff 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.URL.Path), + }) + 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/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 new file mode 100644 index 0000000000..d068b24597 --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/mocks/GatewayClient.go @@ -0,0 +1,434 @@ +// 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" + + gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/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 +} + +// 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)) + 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 +} + +// 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)) + 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 +} + +// 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)) + 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 +} + +// 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)) + 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 1fe1bf5d39..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 @@ -19,52 +19,121 @@ package shares import ( + "context" + "fmt" "net/http" "path" + "sort" + "strconv" 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 := h.getClient() + 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. + base := path.Base(sharedResource.GetInfo().GetPath()) + mount := base + var mountPoints []string + sharesToAccept := map[string]bool{shareID: true} + for _, s := range lrs.Shares { + 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) + + // 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)) + } + } + + for id := range sharesToAccept { + h.updateReceivedShare(w, r, id, 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) - 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 } + // 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 +178,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 GatewayClient, 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 GatewayClient, 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 GatewayClient) (*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/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..cd38f75f6e --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/pending_test.go @@ -0,0 +1,288 @@ +// 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() { + 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/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 d3925dd744..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 @@ -41,6 +41,9 @@ 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" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/ReneKroon/ttlcache/v2" "github.com/bluele/gcache" @@ -49,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" @@ -58,6 +62,8 @@ import ( "github.com/pkg/errors" ) +//go:generate mockery -name GatewayClient + const ( storageIDPrefix string = "shared::" ) @@ -65,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 @@ -72,6 +79,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 @@ -81,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]) @@ -88,9 +103,33 @@ func getCacheWarmupManager(c *config.Config) (cache.Warmup, error) { return nil, fmt.Errorf("driver not found: %s", c.CacheWarmupDriver) } -// Init initializes this and any contained handlers -func (h *Handler) Init(c *config.Config) { +// 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 { + 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) + + 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) + 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) +} + +// 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 @@ -108,6 +147,13 @@ func (h *Handler) Init(c *config.Config) { go h.startCacheWarmup(cwm) } } + h.getClient = h.getPoolClient +} + +// Init initializes the handler +func (h *Handler) Init(c *config.Config, clientGetter GatewayClientGetter) { + h.InitDefault(c) + h.getClient = clientGetter } func (h *Handler) startCacheWarmup(c cache.Warmup) { @@ -132,7 +178,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 @@ -166,21 +212,124 @@ 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 - if role, val, err := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole()); err == nil { - h.createUserShare(w, r, statRes.Info, role, val) + 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 + } + + 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) } - 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) + 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 + } + + err = h.addFileInfo(ctx, s, statRes.Info) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) + return + } + + h.mapUserIds(ctx, client, s) + + 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 + } + + // 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 = 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 + } + } + } } + 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 { @@ -191,7 +340,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 @@ -206,17 +355,26 @@ 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) - } else { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), nil) + return nil, nil, &ocsError{ + Code: http.StatusNotFound, + Message: err.Error(), + Error: err, + } + } + return nil, nil, &ocsError{ + Code: response.MetaBadRequest.StatusCode, + Message: err.Error(), + Error: err, } - return nil, nil, err } role = conversions.RoleFromOCSPermissions(perm) } @@ -228,23 +386,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 @@ -513,7 +680,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 +1045,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 +1123,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 +1168,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,33 +1214,30 @@ 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) (*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 + 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 + 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 - } - s, err := conversions.CS3Share2ShareData(ctx, createShareResponse.Share) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return - } - err = h.addFileInfo(ctx, s, info) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return } - h.mapUserIds(ctx, client, s) - - response.WriteOCSSuccess(w, r, s) + return createShareResponse.Share, nil } func mapState(state collaboration.ShareState) int { @@ -1107,3 +1271,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..1b681efb77 --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares_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 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..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 @@ -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,290 @@ // // 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" + "fmt" + "net/http/httptest" + "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" + "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}, - } - - 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) - } - } -} +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("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) + + user := &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: "admin", + }, + } + + client.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userpb.GetUserByClaimResponse{ + Status: status.NewOK(context.Background()), + 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{ + 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{ + 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) + }) + + 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"` + } + + 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)) + + 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/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 22cc7ef477..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 @@ -30,21 +30,26 @@ 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, *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", + Error: err, + } } userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ @@ -52,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 + 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 + return nil, &ocsError{ + Code: response.MetaNotFound.StatusCode, + Message: "user not found", + Error: err, + } } createShareReq := &collaboration.CreateShareRequest{ @@ -82,13 +93,17 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }, } - 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) { 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 +167,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 } 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) { 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..9347fa613b 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,29 @@ 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) + filtered = append(filtered, rss...) + + 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 +435,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 +476,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..15a08e3375 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,7 +277,7 @@ 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=?)" + 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 ( @@ -310,7 +310,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 +337,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 +378,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 +414,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 +483,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 +505,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 +519,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 +577,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..24d20ede45 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,124 @@ 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 + ) + 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")) + }) + + 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 +371,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 +387,87 @@ 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 + ) + Expect(err).ToNot(HaveOccurred()) + 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 447f9f8593..ec367bfaec 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -49,6 +49,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 @@ -323,28 +324,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 } } @@ -354,16 +363,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/pkg/user/manager/ldap/ldap.go b/pkg/user/manager/ldap/ldap.go index dbaf00b09e..4f72e01896 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,53 @@ 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) + 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) { + 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 +409,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 +430,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/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index 6b7300f141..a1358b6870 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_ @@ -336,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) @@ -524,7 +514,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) @@ -552,12 +541,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)_ @@ -668,8 +651,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) @@ -678,8 +659,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) @@ -692,10 +671,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) @@ -737,38 +712,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) @@ -981,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) @@ -1328,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) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index 64d79f5f95..cec4fea7e9 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) @@ -338,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) @@ -349,7 +339,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) @@ -530,12 +519,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)_ @@ -674,7 +657,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) @@ -683,8 +665,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) @@ -697,10 +677,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) @@ -742,38 +718,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) @@ -985,12 +943,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) @@ -1332,10 +1284,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) diff --git a/tests/oc-integration-tests/drone/frontend.toml b/tests/oc-integration-tests/drone/frontend.toml index 13fbba77dc..95dd505ba9 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 @@ -61,6 +61,7 @@ files_namespace = "/users" 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 6d2577c2ca..eb6879da8e 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" @@ -79,6 +80,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"} +"/users" = {"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/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" 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/drone/storage-shares.toml b/tests/oc-integration-tests/drone/storage-shares.toml new file mode 100644 index 0000000000..97403ef3fa --- /dev/null +++ b/tests/oc-integration-tests/drone/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/frontend.toml b/tests/oc-integration-tests/local/frontend.toml index 7350e7b2a9..4aec478e17 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 = "/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 @@ -61,6 +61,7 @@ files_namespace = "/users" 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 717bc766ca..44ab2858b3 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" @@ -72,6 +73,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"} +"/users" = {"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/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" 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 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