mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-13 23:49:05 -06:00
Space Trash-bin expiration cli (#5500)
* add storage-users trash-bin cli add task to clean up outdated trash-bin resources add trash-bin cli purge-expired command to purge expired trash-bin resources add purge-expired task tests
This commit is contained in:
23
changelog/unreleased/space-trashbin-purge-cli.md
Normal file
23
changelog/unreleased/space-trashbin-purge-cli.md
Normal file
@@ -0,0 +1,23 @@
|
||||
Enhancement: Cli to purge expired trash-bin items
|
||||
|
||||
Introduction of a new cli command to purge old trash-bin items.
|
||||
The command is part of the `storage-users` service and can be used as follows:
|
||||
|
||||
`ocis storage-users trash-bin purge-expired`.
|
||||
|
||||
The `purge-expired` command configuration is done in the `ocis`configuration or as usual by using environment variables.
|
||||
|
||||
ENV `STORAGE_USERS_PURGE_TRASH_BIN_USER_ID` is used to obtain space trash-bin information and takes the system admin user as the default `OCIS_ADMIN_USER_ID`.
|
||||
It should be noted, that this is only set by default in the single binary. The command only considers spaces to which the user has access and delete permission.
|
||||
|
||||
ENV `STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
|
||||
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `personal space` trash-bin files.
|
||||
|
||||
ENV `STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
|
||||
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `project space` trash-bin files.
|
||||
|
||||
Likewise, only spaces of the type `project` and `personal` are taken into account.
|
||||
Spaces of type `virtual`, for example, are ignored.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/5500
|
||||
https://github.com/owncloud/ocis/issues/5499
|
||||
@@ -19,6 +19,7 @@ func GetCommands(cfg *config.Config) cli.Commands {
|
||||
|
||||
// interaction with this service
|
||||
Uploads(cfg),
|
||||
TrashBin(cfg),
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/cs3org/reva/v2/cmd/revad/runtime"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/logging"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/revaconfig"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/server/debug"
|
||||
@@ -88,6 +90,27 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
logger.Fatal().Err(err).Msg("failed to register the grpc endpoint")
|
||||
}
|
||||
|
||||
{
|
||||
stream, err := event.NewStream(cfg.Events)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("can't connect to nats")
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(cfg.Reva.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventSVC, err := event.NewService(gw, stream, logger, *cfg)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("can't create event service")
|
||||
}
|
||||
|
||||
gr.Add(eventSVC.Run, func(_ error) {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
|
||||
55
services/storage-users/pkg/command/trash_bin.go
Normal file
55
services/storage-users/pkg/command/trash_bin.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// TrashBin wraps trash-bin related sub-commands.
|
||||
func TrashBin(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "trash-bin",
|
||||
Usage: "manage trash-bin's",
|
||||
Subcommands: []*cli.Command{
|
||||
PurgeExpiredResources(cfg),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PurgeExpiredResources cli command removes old trash-bin items.
|
||||
func PurgeExpiredResources(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "purge-expired",
|
||||
Usage: "Purge expired trash-bin items",
|
||||
Flags: []cli.Flag{},
|
||||
Before: func(c *cli.Context) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
stream, err := event.NewStream(cfg.Events)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := events.Publish(stream, event.PurgeTrashBin{ExecutionTime: time.Now()}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// go-micro nats implementation uses async publishing,
|
||||
// therefore we need to manually wait.
|
||||
//
|
||||
// FIXME: upstream pr
|
||||
//
|
||||
// https://github.com/go-micro/plugins/blob/3e77393890683be4bacfb613bc5751867d584692/v4/events/natsjs/nats.go#L115
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
@@ -32,6 +33,7 @@ type Config struct {
|
||||
ExposeDataServer bool `yaml:"expose_data_server" env:"STORAGE_USERS_EXPOSE_DATA_SERVER" desc:"Exposes the data server directly to users and bypasses the data gateway. Ensure that the data server address is reachable by users."`
|
||||
ReadOnly bool `yaml:"readonly" env:"STORAGE_USERS_READ_ONLY" desc:"Set this storage to be read-only."`
|
||||
UploadExpiration int64 `yaml:"upload_expiration" env:"STORAGE_USERS_UPLOAD_EXPIRATION" desc:"Duration in seconds after which uploads will expire."`
|
||||
Tasks Tasks `yaml:"tasks"`
|
||||
|
||||
Supervised bool `yaml:"-"`
|
||||
Context context.Context `yaml:"-"`
|
||||
@@ -218,3 +220,15 @@ type LocalDriver struct {
|
||||
ShareFolder string `yaml:"share_folder"`
|
||||
UserLayout string `yaml:"user_layout"`
|
||||
}
|
||||
|
||||
// Tasks wraps task configurations
|
||||
type Tasks struct {
|
||||
PurgeTrashBin PurgeTrashBin `yaml:"purge_trash_bin"`
|
||||
}
|
||||
|
||||
// PurgeTrashBin contains all necessary configurations to clean up the respective trash cans
|
||||
type PurgeTrashBin struct {
|
||||
UserID string `yaml:"user_id" env:"OCIS_ADMIN_USER_ID;STORAGE_USERS_PURGE_TRASH_BIN_USER_ID" desc:"ID of the user who collects all necessary information for deletion."`
|
||||
PersonalDeleteBefore time.Duration `yaml:"personal_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the personal trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
|
||||
ProjectDeleteBefore time.Duration `yaml:"project_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the project trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package defaults
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
@@ -87,6 +88,12 @@ func DefaultConfig() *config.Config {
|
||||
Store: "memory",
|
||||
Database: "users",
|
||||
},
|
||||
Tasks: config.Tasks{
|
||||
PurgeTrashBin: config.PurgeTrashBin{
|
||||
ProjectDeleteBefore: 30 * 24 * time.Hour,
|
||||
PersonalDeleteBefore: 30 * 24 * time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +146,10 @@ func EnsureDefaults(cfg *config.Config) {
|
||||
cfg.GRPC.TLS.Key = cfg.Commons.GRPCServiceTLS.Key
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Tasks.PurgeTrashBin.UserID == "" && cfg.Commons != nil {
|
||||
cfg.Tasks.PurgeTrashBin.UserID = cfg.Commons.AdminUserID
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
|
||||
51
services/storage-users/pkg/event/event.go
Normal file
51
services/storage-users/pkg/event/event.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events/stream"
|
||||
"github.com/go-micro/plugins/v4/events/natsjs"
|
||||
ociscrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
"go-micro.dev/v4/events"
|
||||
)
|
||||
|
||||
// NewStream prepares the requested nats stream and returns it.
|
||||
func NewStream(cfg config.Events) (events.Stream, error) {
|
||||
var tlsConf *tls.Config
|
||||
|
||||
if cfg.EnableTLS {
|
||||
var rootCAPool *x509.CertPool
|
||||
if cfg.TLSRootCaCertPath != "" {
|
||||
rootCrtFile, err := os.Open(cfg.TLSRootCaCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootCAPool, err = ociscrypto.NewCertPoolFromPEM(rootCrtFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.TLSInsecure = false
|
||||
}
|
||||
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
RootCAs: rootCAPool,
|
||||
}
|
||||
}
|
||||
|
||||
s, err := stream.Nats(
|
||||
natsjs.TLSConfig(tlsConf),
|
||||
natsjs.Address(cfg.Addr),
|
||||
natsjs.ClusterID(cfg.ClusterID),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
85
services/storage-users/pkg/event/service.go
Normal file
85
services/storage-users/pkg/event/service.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
|
||||
)
|
||||
|
||||
const (
|
||||
consumerGroup = "storage-users"
|
||||
)
|
||||
|
||||
// Service wraps all common logic that is needed to react to incoming events.
|
||||
type Service struct {
|
||||
gatewayClient apiGateway.GatewayAPIClient
|
||||
eventStream events.Stream
|
||||
logger log.Logger
|
||||
config config.Config
|
||||
}
|
||||
|
||||
// NewService prepares and returns a Service implementation.
|
||||
func NewService(gatewayClient apiGateway.GatewayAPIClient, eventStream events.Stream, logger log.Logger, conf config.Config) (Service, error) {
|
||||
svc := Service{
|
||||
gatewayClient: gatewayClient,
|
||||
eventStream: eventStream,
|
||||
logger: logger,
|
||||
config: conf,
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// Run to fulfil Runner interface
|
||||
func (s Service) Run() error {
|
||||
ch, err := events.Consume(s.eventStream, consumerGroup, PurgeTrashBin{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for e := range ch {
|
||||
var errs []error
|
||||
|
||||
switch ev := e.(type) {
|
||||
case PurgeTrashBin:
|
||||
executionTime := ev.ExecutionTime
|
||||
if executionTime.IsZero() {
|
||||
executionTime = time.Now()
|
||||
}
|
||||
|
||||
executantID := ev.ExecutantID
|
||||
if executantID == nil {
|
||||
executantID = &apiUser.UserId{OpaqueId: s.config.Tasks.PurgeTrashBin.UserID}
|
||||
}
|
||||
|
||||
tasks := map[task.SpaceType]time.Time{
|
||||
task.Project: executionTime.Add(-s.config.Tasks.PurgeTrashBin.ProjectDeleteBefore),
|
||||
task.Personal: executionTime.Add(-s.config.Tasks.PurgeTrashBin.PersonalDeleteBefore),
|
||||
}
|
||||
|
||||
for spaceType, deleteBefore := range tasks {
|
||||
// skip task execution if the deleteBefore time is the same as the now time,
|
||||
// which indicates that the duration configuration for this space type is set to 0 which is the equivalent to disabled.
|
||||
if deleteBefore.Equal(executionTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = task.PurgeTrashBin(executantID, deleteBefore, spaceType, s.gatewayClient, s.config.Commons.MachineAuthAPIKey); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, err := range errs {
|
||||
s.logger.Error().Err(err).Interface("event", e)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
services/storage-users/pkg/event/trigger.go
Normal file
21
services/storage-users/pkg/event/trigger.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
)
|
||||
|
||||
// PurgeTrashBin wraps all needed information to purge a trash-bin
|
||||
type PurgeTrashBin struct {
|
||||
ExecutantID *apiUser.UserId
|
||||
ExecutionTime time.Time
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
func (PurgeTrashBin) Unmarshal(v []byte) (interface{}, error) {
|
||||
e := PurgeTrashBin{}
|
||||
err := json.Unmarshal(v, &e)
|
||||
return e, err
|
||||
}
|
||||
11
services/storage-users/pkg/task/task.go
Normal file
11
services/storage-users/pkg/task/task.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package task
|
||||
|
||||
// SpaceType represents known space types
|
||||
type SpaceType string
|
||||
|
||||
const (
|
||||
// Personal represents a space of type personal
|
||||
Personal SpaceType = "personal"
|
||||
// Project represents a space of type project
|
||||
Project SpaceType = "project"
|
||||
)
|
||||
13
services/storage-users/pkg/task/task_suite_test.go
Normal file
13
services/storage-users/pkg/task/task_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTask(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Task Suite")
|
||||
}
|
||||
112
services/storage-users/pkg/task/trash_bin.go
Normal file
112
services/storage-users/pkg/task/trash_bin.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// PurgeTrashBin can be used to purge space trash-bin's,
|
||||
// the provided executantID must have space access.
|
||||
// removeBefore specifies how long an item must be in the trash-bin to be deleted,
|
||||
// items that stay there for a shorter time are ignored and kept in place.
|
||||
func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceType SpaceType, gwc apiGateway.GatewayAPIClient, machineAuthAPIKey string) error {
|
||||
executantCtx, _, err := utils.Impersonate(executantID, gwc, machineAuthAPIKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listStorageSpacesResponse, err := gwc.ListStorageSpaces(executantCtx, &apiProvider.ListStorageSpacesRequest{
|
||||
Filters: []*apiProvider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: apiProvider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
|
||||
Term: &apiProvider.ListStorageSpacesRequest_Filter_SpaceType{
|
||||
SpaceType: string(spaceType),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, storageSpace := range listStorageSpacesResponse.StorageSpaces {
|
||||
var (
|
||||
err error
|
||||
impersonationID *apiUser.UserId
|
||||
storageSpaceReference = &apiProvider.Reference{
|
||||
ResourceId: storageSpace.GetRoot(),
|
||||
}
|
||||
)
|
||||
|
||||
switch SpaceType(storageSpace.GetSpaceType()) {
|
||||
case Personal:
|
||||
impersonationID = storageSpace.GetOwner().GetId()
|
||||
case Project:
|
||||
var permissionsMap map[string]*apiProvider.ResourcePermissions
|
||||
err := utils.ReadJSONFromOpaque(storageSpace.GetOpaque(), "grants", &permissionsMap)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
for id, permissions := range permissionsMap {
|
||||
if !permissions.Delete {
|
||||
continue
|
||||
}
|
||||
|
||||
impersonationID = &apiUser.UserId{
|
||||
OpaqueId: id,
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if impersonationID == nil {
|
||||
return fmt.Errorf("can't impersonate space user for space: %s", storageSpace.GetId().GetOpaqueId())
|
||||
}
|
||||
|
||||
impersonatedCtx, _, err := utils.Impersonate(impersonationID, gwc, machineAuthAPIKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listRecycleResponse, err := gwc.ListRecycle(impersonatedCtx, &apiProvider.ListRecycleRequest{Ref: storageSpaceReference})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, recycleItem := range listRecycleResponse.GetRecycleItems() {
|
||||
doDelete := utils.TSToUnixNano(recycleItem.DeletionTime) < utils.TSToUnixNano(utils.TimeToTS(deleteBefore))
|
||||
if !doDelete {
|
||||
continue
|
||||
}
|
||||
|
||||
purgeRecycleResponse, err := gwc.PurgeRecycle(impersonatedCtx, &apiProvider.PurgeRecycleRequest{
|
||||
Ref: storageSpaceReference,
|
||||
Key: recycleItem.Key,
|
||||
})
|
||||
|
||||
if purgeRecycleResponse.GetStatus().GetCode() != apiRpc.Code_CODE_OK {
|
||||
return errtypes.NewErrtypeFromStatus(purgeRecycleResponse.Status)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
230
services/storage-users/pkg/task/trash_bin_test.go
Normal file
230
services/storage-users/pkg/task/trash_bin_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"google.golang.org/grpc"
|
||||
"time"
|
||||
|
||||
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
apiTypes "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func MustMarshal(v any) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var _ = Describe("trash", func() {
|
||||
var (
|
||||
gwc *cs3mocks.GatewayAPIClient
|
||||
ctx context.Context
|
||||
now time.Time
|
||||
genericError error
|
||||
user *apiUser.User
|
||||
getUserResponse *apiUser.GetUserResponse
|
||||
authenticateResponse *apiGateway.AuthenticateResponse
|
||||
listStorageSpacesResponse *apiProvider.ListStorageSpacesResponse
|
||||
personalSpace *apiProvider.StorageSpace
|
||||
projectSpace *apiProvider.StorageSpace
|
||||
virtualSpace *apiProvider.StorageSpace
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
gwc = &cs3mocks.GatewayAPIClient{}
|
||||
ctx = context.Background()
|
||||
now = time.Now()
|
||||
genericError = errors.New("any")
|
||||
getUserResponse = &apiUser.GetUserResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
}
|
||||
authenticateResponse = &apiGateway.AuthenticateResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Token: "",
|
||||
}
|
||||
listStorageSpacesResponse = &apiProvider.ListStorageSpacesResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
StorageSpaces: []*apiProvider.StorageSpace{},
|
||||
}
|
||||
personalSpace = &apiProvider.StorageSpace{
|
||||
SpaceType: "personal",
|
||||
Id: &apiProvider.StorageSpaceId{
|
||||
OpaqueId: "personal",
|
||||
},
|
||||
Root: &apiProvider.ResourceId{
|
||||
OpaqueId: "personal",
|
||||
},
|
||||
}
|
||||
projectSpace = &apiProvider.StorageSpace{
|
||||
SpaceType: "project",
|
||||
Id: &apiProvider.StorageSpaceId{
|
||||
OpaqueId: "project",
|
||||
},
|
||||
Root: &apiProvider.ResourceId{
|
||||
OpaqueId: "project",
|
||||
},
|
||||
Opaque: &apiTypes.Opaque{},
|
||||
}
|
||||
// virtual is here as an example,
|
||||
// the task ignores all space types expect `project` and `personal`.
|
||||
virtualSpace = &apiProvider.StorageSpace{
|
||||
SpaceType: "virtual",
|
||||
Id: &apiProvider.StorageSpaceId{
|
||||
OpaqueId: "virtual",
|
||||
},
|
||||
Root: &apiProvider.ResourceId{
|
||||
OpaqueId: "virtual",
|
||||
},
|
||||
}
|
||||
user = &apiUser.User{
|
||||
Id: &apiUser.UserId{
|
||||
OpaqueId: "user",
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Describe("PurgeTrashBin", func() {
|
||||
It("throws an error if the user cannot authenticate", func() {
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(nil, genericError)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("throws an error if space listing fails", func() {
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
|
||||
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(nil, genericError)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("throws an error if a personal space user can't be impersonated", func() {
|
||||
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{personalSpace}
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
|
||||
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: personal")))
|
||||
})
|
||||
It("throws an error if a project space user can't be impersonated", func() {
|
||||
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
|
||||
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
|
||||
})
|
||||
It("throws an error if a project space has no user with delete permissions", func() {
|
||||
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
|
||||
projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
|
||||
"grants": {
|
||||
Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
|
||||
"admin": {
|
||||
Delete: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
|
||||
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
|
||||
})
|
||||
It("only deletes items older than the specified period", func() {
|
||||
var (
|
||||
recycleItems = map[string][]*apiProvider.RecycleItem{
|
||||
"personal": {
|
||||
{Key: "now", DeletionTime: utils.TimeToTS(now)},
|
||||
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Second))},
|
||||
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Second))},
|
||||
},
|
||||
"project": {
|
||||
{Key: "now", DeletionTime: utils.TimeToTS(now)},
|
||||
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Minute))},
|
||||
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Minute))},
|
||||
},
|
||||
"virtual": {
|
||||
{Key: "now", DeletionTime: utils.TimeToTS(now)},
|
||||
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Hour))},
|
||||
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Hour))},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
personalSpace.Owner = user
|
||||
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{
|
||||
personalSpace,
|
||||
projectSpace,
|
||||
virtualSpace,
|
||||
}
|
||||
projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
|
||||
"grants": {
|
||||
Decoder: "json",
|
||||
Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
|
||||
"admin": {
|
||||
Delete: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
|
||||
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
|
||||
gwc.On("ListRecycle", mock.Anything, mock.Anything).Return(
|
||||
func(_ context.Context, req *apiProvider.ListRecycleRequest, _ ...grpc.CallOption) *apiProvider.ListRecycleResponse {
|
||||
return &apiProvider.ListRecycleResponse{
|
||||
RecycleItems: recycleItems[req.Ref.ResourceId.OpaqueId],
|
||||
}
|
||||
}, nil,
|
||||
)
|
||||
gwc.On("PurgeRecycle", mock.Anything, mock.Anything).Return(
|
||||
func(_ context.Context, req *apiProvider.PurgeRecycleRequest, _ ...grpc.CallOption) *apiProvider.PurgeRecycleResponse {
|
||||
var items []*apiProvider.RecycleItem
|
||||
for _, item := range recycleItems[req.Ref.ResourceId.OpaqueId] {
|
||||
if req.Key == item.Key {
|
||||
continue
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
recycleItems[req.Ref.ResourceId.OpaqueId] = items
|
||||
|
||||
return &apiProvider.PurgeRecycleResponse{
|
||||
Status: &apiRpc.Status{
|
||||
Code: apiRpc.Code_CODE_OK,
|
||||
},
|
||||
}
|
||||
}, nil,
|
||||
)
|
||||
|
||||
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(recycleItems["personal"]).To(HaveLen(2))
|
||||
Expect(recycleItems["project"]).To(HaveLen(2))
|
||||
// virtual spaces are ignored
|
||||
Expect(recycleItems["virtual"]).To(HaveLen(3))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user