// Copyright (C) 2019-2023 Nicola Murino // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, version 3. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package common import ( "bytes" "crypto/rand" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" "os" "path" "path/filepath" "runtime" "strings" "testing" "time" "github.com/klauspost/compress/zip" "github.com/rs/xid" "github.com/sftpgo/sdk" sdkkms "github.com/sftpgo/sdk/kms" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/kms" "github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/vfs" ) func TestEventRuleMatch(t *testing.T) { role := "role1" conditions := &dataprovider.EventConditions{ ProviderEvents: []string{"add", "update"}, Options: dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "user1", InverseMatch: true, }, }, RoleNames: []dataprovider.ConditionPattern{ { Pattern: role, }, }, }, } res := eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user1", Role: role, Event: "add", }) assert.False(t, res) res = eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user2", Role: role, Event: "update", }) assert.True(t, res) res = eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user2", Role: role, Event: "delete", }) assert.False(t, res) conditions.Options.ProviderObjects = []string{"api_key"} res = eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user2", Event: "update", Role: role, ObjectType: "share", }) assert.False(t, res) res = eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user2", Event: "update", Role: role, ObjectType: "api_key", }) assert.True(t, res) res = eventManager.checkProviderEventMatch(conditions, &EventParams{ Name: "user2", Event: "update", Role: role + "1", ObjectType: "api_key", }) assert.False(t, res) // now test fs events conditions = &dataprovider.EventConditions{ FsEvents: []string{operationUpload, operationDownload}, Options: dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "user*", }, { Pattern: "tester*", }, }, RoleNames: []dataprovider.ConditionPattern{ { Pattern: role, InverseMatch: true, }, }, FsPaths: []dataprovider.ConditionPattern{ { Pattern: "/**/*.txt", }, }, Protocols: []string{ProtocolSFTP}, MinFileSize: 10, MaxFileSize: 30, }, } params := EventParams{ Name: "tester4", Event: operationDelete, VirtualPath: "/path.txt", Protocol: ProtocolSFTP, ObjectName: "path.txt", FileSize: 20, } res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Event = operationDownload res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.True(t, res) params.Role = role res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Role = "" params.Name = "name" res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Name = "user5" res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.True(t, res) params.VirtualPath = "/sub/f.jpg" params.ObjectName = path.Base(params.VirtualPath) res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.VirtualPath = "/sub/f.txt" params.ObjectName = path.Base(params.VirtualPath) res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.True(t, res) params.Protocol = ProtocolHTTP res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Protocol = ProtocolSFTP params.FileSize = 5 res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.FileSize = 50 res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.FileSize = 25 res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.True(t, res) // bad pattern conditions.Options.Names = []dataprovider.ConditionPattern{ { Pattern: "[-]", }, } res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) // check fs events with group name filters conditions = &dataprovider.EventConditions{ FsEvents: []string{operationUpload, operationDownload}, Options: dataprovider.ConditionOptions{ GroupNames: []dataprovider.ConditionPattern{ { Pattern: "group*", }, { Pattern: "testgroup*", }, }, }, } params = EventParams{ Name: "user1", Event: operationUpload, } res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Groups = []sdk.GroupMapping{ { Name: "g1", Type: sdk.GroupTypePrimary, }, { Name: "g2", Type: sdk.GroupTypeSecondary, }, } res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.False(t, res) params.Groups = []sdk.GroupMapping{ { Name: "testgroup2", Type: sdk.GroupTypePrimary, }, { Name: "g2", Type: sdk.GroupTypeSecondary, }, } res = eventManager.checkFsEventMatch(conditions, ¶ms) assert.True(t, res) // check user conditions user := dataprovider.User{} user.Username = "u1" res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{}) assert.True(t, res) res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "user", }, }, }) assert.False(t, res) res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{ RoleNames: []dataprovider.ConditionPattern{ { Pattern: role, }, }, }) assert.False(t, res) user.Role = role res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{ RoleNames: []dataprovider.ConditionPattern{ { Pattern: role, }, }, }) assert.True(t, res) res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{ GroupNames: []dataprovider.ConditionPattern{ { Pattern: "group", }, }, RoleNames: []dataprovider.ConditionPattern{ { Pattern: role, }, }, }) assert.False(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 0, }, &EventParams{ Event: IDPLoginAdmin, }) assert.True(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 2, }, &EventParams{ Event: IDPLoginAdmin, }) assert.True(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 1, }, &EventParams{ Event: IDPLoginAdmin, }) assert.False(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 1, }, &EventParams{ Event: IDPLoginUser, }) assert.True(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 1, }, &EventParams{ Name: "user", Event: IDPLoginUser, }) assert.True(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 1, Options: dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "abc", }, }, }, }, &EventParams{ Name: "user", Event: IDPLoginUser, }) assert.False(t, res) res = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{ IDPLoginEvent: 2, }, &EventParams{ Name: "user", Event: IDPLoginUser, }) assert.False(t, res) } func TestDoubleStarMatching(t *testing.T) { c := dataprovider.ConditionPattern{ Pattern: "/mydir/**", } res := checkEventConditionPattern(c, "/mydir") assert.True(t, res) res = checkEventConditionPattern(c, "/mydirname") assert.False(t, res) res = checkEventConditionPattern(c, "/mydir/sub") assert.True(t, res) res = checkEventConditionPattern(c, "/mydir/sub/dir") assert.True(t, res) c.Pattern = "/**/*" res = checkEventConditionPattern(c, "/mydir") assert.True(t, res) res = checkEventConditionPattern(c, "/mydirname") assert.True(t, res) res = checkEventConditionPattern(c, "/mydir/sub/dir/file.txt") assert.True(t, res) c.Pattern = "/mydir/**/*.txt" res = checkEventConditionPattern(c, "/mydir") assert.False(t, res) res = checkEventConditionPattern(c, "/mydirname/f.txt") assert.False(t, res) res = checkEventConditionPattern(c, "/mydir/sub") assert.False(t, res) res = checkEventConditionPattern(c, "/mydir/sub/dir") assert.False(t, res) res = checkEventConditionPattern(c, "/mydir/sub/dir/a.txt") assert.True(t, res) } func TestEventManager(t *testing.T) { startEventScheduler() action := &dataprovider.BaseEventAction{ Name: "test_action", Type: dataprovider.ActionTypeHTTP, Options: dataprovider.BaseEventActionOptions{ HTTPConfig: dataprovider.EventActionHTTPConfig{ Endpoint: "http://localhost", Timeout: 20, Method: http.MethodGet, }, }, } err := dataprovider.AddEventAction(action, "", "", "") assert.NoError(t, err) rule := &dataprovider.EventRule{ Name: "rule", Status: 1, Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{operationUpload}, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action.Name, }, Order: 1, }, }, } err = dataprovider.AddEventRule(rule, "", "", "") assert.NoError(t, err) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 1) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) assert.Len(t, eventManager.schedulesMapping, 0) eventManager.RUnlock() rule.Trigger = dataprovider.EventTriggerProviderEvent rule.Conditions = dataprovider.EventConditions{ ProviderEvents: []string{"add"}, } err = dataprovider.UpdateEventRule(rule, "", "", "") assert.NoError(t, err) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 1) assert.Len(t, eventManager.Schedules, 0) assert.Len(t, eventManager.schedulesMapping, 0) eventManager.RUnlock() rule.Trigger = dataprovider.EventTriggerSchedule rule.Conditions = dataprovider.EventConditions{ Schedules: []dataprovider.Schedule{ { Hours: "0", DayOfWeek: "*", DayOfMonth: "*", Month: "*", }, }, } rule.DeletedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-12 * time.Hour)) eventManager.addUpdateRuleInternal(*rule) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) assert.Len(t, eventManager.schedulesMapping, 0) eventManager.RUnlock() assert.Eventually(t, func() bool { _, err = dataprovider.EventRuleExists(rule.Name) ok := errors.Is(err, util.ErrNotFound) return ok }, 2*time.Second, 100*time.Millisecond) rule.DeletedAt = 0 err = dataprovider.AddEventRule(rule, "", "", "") assert.NoError(t, err) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 1) assert.Len(t, eventManager.schedulesMapping, 1) eventManager.RUnlock() err = dataprovider.DeleteEventRule(rule.Name, "", "", "") assert.NoError(t, err) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) assert.Len(t, eventManager.schedulesMapping, 0) eventManager.RUnlock() err = dataprovider.DeleteEventAction(action.Name, "", "", "") assert.NoError(t, err) stopEventScheduler() } func TestEventManagerErrors(t *testing.T) { startEventScheduler() providerConf := dataprovider.GetProviderConfig() err := dataprovider.Close() assert.NoError(t, err) params := EventParams{ sender: "sender", } _, err = params.getUsers() assert.Error(t, err) _, err = params.getFolders() assert.Error(t, err) err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeMetadataCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeCopyFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{}, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) _, err = executeAdminCheckAction(&dataprovider.EventActionIDPAccountCheck{}, &EventParams{}) assert.Error(t, err) _, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{}, &EventParams{}) assert.Error(t, err) groupName := "agroup" err = executeQuotaResetForUser(&dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeMetadataCheckForUser(&dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeDataRetentionCheckForUser(dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }, nil, &EventParams{}, "") assert.Error(t, err) err = executeDeleteFsActionForUser(nil, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeMkDirsFsActionForUser(nil, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeRenameFsActionForUser(nil, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeExistFsActionForUser(nil, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeCopyFsActionForUser(nil, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, nil, dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }) assert.Error(t, err) err = executePwdExpirationCheckForUser(&dataprovider.User{ Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }}, dataprovider.EventActionPasswordExpiration{}) assert.Error(t, err) _, _, err = getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{ Method: http.MethodPost, Parts: []dataprovider.HTTPPart{ { Name: "p1", }, }, }, nil, nil, dataprovider.User{ BaseUser: sdk.BaseUser{ Username: "u", }, Groups: []sdk.GroupMapping{ { Name: groupName, Type: sdk.GroupTypePrimary, }, }, }, &EventParams{}, false) assert.Error(t, err) dataRetentionAction := dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeDataRetentionCheck, Options: dataprovider.BaseEventActionOptions{ RetentionConfig: dataprovider.EventActionDataRetentionConfig{ Folders: []dataprovider.FolderRetention{ { Path: "/", Retention: 24, }, }, }, }, } err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "username1", }, }, }) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unable to get users") } eventManager.loadRules() eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) eventManager.RUnlock() // rule with invalid trigger eventManager.addUpdateRuleInternal(dataprovider.EventRule{ Name: "test rule", Status: 1, Trigger: -1, }) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) eventManager.RUnlock() // rule with invalid cronspec eventManager.addUpdateRuleInternal(dataprovider.EventRule{ Name: "test rule", Status: 1, Trigger: dataprovider.EventTriggerSchedule, Conditions: dataprovider.EventConditions{ Schedules: []dataprovider.Schedule{ { Hours: "1000", }, }, }, }) eventManager.RLock() assert.Len(t, eventManager.FsEvents, 0) assert.Len(t, eventManager.ProviderEvents, 0) assert.Len(t, eventManager.Schedules, 0) eventManager.RUnlock() err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) stopEventScheduler() } func TestEventRuleActions(t *testing.T) { actionName := "test rule action" action := dataprovider.BaseEventAction{ Name: actionName, Type: dataprovider.ActionTypeBackup, } err := executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.NoError(t, err) action.Type = -1 err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.Error(t, err) action = dataprovider.BaseEventAction{ Name: actionName, Type: dataprovider.ActionTypeHTTP, Options: dataprovider.BaseEventActionOptions{ HTTPConfig: dataprovider.EventActionHTTPConfig{ Endpoint: "http://foo\x7f.com/", // invalid URL SkipTLSVerify: true, Body: "{{ObjectData}}", Method: http.MethodPost, QueryParameters: []dataprovider.KeyValue{ { Key: "param", Value: "value", }, }, Timeout: 5, Headers: []dataprovider.KeyValue{ { Key: "Content-Type", Value: "application/json", }, }, Username: "httpuser", }, }, } action.Options.SetEmptySecretsIfNil() err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "invalid endpoint") } action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v", httpAddr) params := &EventParams{ Name: "a", Object: &dataprovider.User{ BaseUser: sdk.BaseUser{ Username: "test user", }, }, } err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) assert.NoError(t, err) action.Options.HTTPConfig.Method = http.MethodGet err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) assert.NoError(t, err) action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v/404", httpAddr) err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unexpected status code: 404") } action.Options.HTTPConfig.Endpoint = "http://invalid:1234" err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) assert.Error(t, err) action.Options.HTTPConfig.QueryParameters = nil action.Options.HTTPConfig.Endpoint = "http://bar\x7f.com/" err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) assert.Error(t, err) action.Options.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, "payload", "key", "data") err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unable to decrypt HTTP password") } action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v", httpAddr) action.Options.HTTPConfig.Password = kms.NewEmptySecret() action.Options.HTTPConfig.Body = "" action.Options.HTTPConfig.Parts = []dataprovider.HTTPPart{ { Name: "p1", Filepath: "path", }, } err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) assert.Contains(t, getErrorString(err), "error getting user") action.Options.HTTPConfig.Parts = nil action.Options.HTTPConfig.Body = "{{ObjectData}}" // test disk and transfer quota reset username1 := "user1" username2 := "user2" user1 := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username1, HomeDir: filepath.Join(os.TempDir(), username1), Status: 1, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, }, } user2 := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username2, HomeDir: filepath.Join(os.TempDir(), username2), Status: 1, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, }, } user2.Filters.PasswordExpiration = 10 err = dataprovider.AddUser(&user1, "", "", "") assert.NoError(t, err) err = dataprovider.AddUser(&user2, "", "", "") assert.NoError(t, err) err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{ Threshold: 20, }, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: user2.Username, }, }, }, &EventParams{}) // smtp not configured assert.Error(t, err) action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeUserQuotaReset, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) // no home dir // create the home dir err = os.MkdirAll(user1.GetHomeDir(), os.ModePerm) assert.NoError(t, err) err = os.WriteFile(filepath.Join(user1.GetHomeDir(), "file.txt"), []byte("user"), 0666) assert.NoError(t, err) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) userGet, err := dataprovider.UserExists(username1, "") assert.NoError(t, err) assert.Equal(t, 1, userGet.UsedQuotaFiles) assert.Equal(t, int64(4), userGet.UsedQuotaSize) // simulate another quota scan in progress assert.True(t, QuotaScans.AddUserQuotaScan(username1, "")) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) assert.True(t, QuotaScans.RemoveUserQuotaScan(username1)) // non matching pattern err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "don't match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no user quota reset executed") action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeMetadataCheck, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "don't match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no metadata check executed") err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) // simulate another metadata check in progress assert.True(t, ActiveMetadataChecks.Add(username1, "")) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) assert.True(t, ActiveMetadataChecks.Remove(username1)) action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeUserExpirationCheck, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "don't match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no user expiration check executed") err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) dataRetentionAction := dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeDataRetentionCheck, Options: dataprovider.BaseEventActionOptions{ RetentionConfig: dataprovider.EventActionDataRetentionConfig{ Folders: []dataprovider.FolderRetention{ { Path: "", Retention: 24, }, }, }, }, } err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) // invalid config, no folder path specified retentionDir := "testretention" dataRetentionAction = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeDataRetentionCheck, Options: dataprovider.BaseEventActionOptions{ RetentionConfig: dataprovider.EventActionDataRetentionConfig{ Folders: []dataprovider.FolderRetention{ { Path: path.Join("/", retentionDir), Retention: 24, DeleteEmptyDirs: true, }, }, }, }, } // create some test files file1 := filepath.Join(user1.GetHomeDir(), "file1.txt") file2 := filepath.Join(user1.GetHomeDir(), retentionDir, "file2.txt") file3 := filepath.Join(user1.GetHomeDir(), retentionDir, "file3.txt") file4 := filepath.Join(user1.GetHomeDir(), retentionDir, "sub", "file4.txt") err = os.MkdirAll(filepath.Dir(file4), os.ModePerm) assert.NoError(t, err) for _, f := range []string{file1, file2, file3, file4} { err = os.WriteFile(f, []byte(""), 0666) assert.NoError(t, err) } timeBeforeRetention := time.Now().Add(-48 * time.Hour) err = os.Chtimes(file1, timeBeforeRetention, timeBeforeRetention) assert.NoError(t, err) err = os.Chtimes(file2, timeBeforeRetention, timeBeforeRetention) assert.NoError(t, err) err = os.Chtimes(file4, timeBeforeRetention, timeBeforeRetention) assert.NoError(t, err) err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) assert.FileExists(t, file1) assert.NoFileExists(t, file2) assert.FileExists(t, file3) assert.NoDirExists(t, filepath.Dir(file4)) // simulate another check in progress c := RetentionChecks.Add(RetentionCheck{}, &user1) assert.NotNil(t, c) err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) RetentionChecks.remove(user1.Username) err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no retention check executed") // test file exists action action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFilesystem, Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionExist, Exist: []string{"/file1.txt", path.Join("/", retentionDir, "file3.txt")}, }, }, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no existence check executed") err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) action.Options.FsConfig.Exist = []string{"/file1.txt", path.Join("/", retentionDir, "file2.txt")} err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.Error(t, err) err = os.RemoveAll(user1.GetHomeDir()) assert.NoError(t, err) err = dataprovider.UpdateUserTransferQuota(&user1, 100, 100, true) assert.NoError(t, err) action.Type = dataprovider.ActionTypeTransferQuotaReset err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, }, }, }) assert.NoError(t, err) userGet, err = dataprovider.UserExists(username1, "") assert.NoError(t, err) assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer) assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no transfer quota reset executed") action.Type = dataprovider.ActionTypeFilesystem action.Options = dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionRename, Renames: []dataprovider.KeyValue{ { Key: "/source", Value: "/target", }, }, }, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no rename executed") action.Options = dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionDelete, Deletes: []string{"/dir1"}, }, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no delete executed") action.Options = dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionMkdirs, Deletes: []string{"/dir1"}, }, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no mkdir executed") action.Options = dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ Name: "test.zip", Paths: []string{"/{{VirtualPath}}"}, }, }, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no file/folder compressed") err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ GroupNames: []dataprovider.ConditionPattern{ { Pattern: "no match", }, }, }) assert.Error(t, err) assert.Contains(t, getErrorString(err), "no file/folder compressed") err = dataprovider.DeleteUser(username1, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteUser(username2, "", "", "") assert.NoError(t, err) // test folder quota reset foldername1 := "f1" foldername2 := "f2" folder1 := vfs.BaseVirtualFolder{ Name: foldername1, MappedPath: filepath.Join(os.TempDir(), foldername1), } folder2 := vfs.BaseVirtualFolder{ Name: foldername2, MappedPath: filepath.Join(os.TempDir(), foldername2), } err = dataprovider.AddFolder(&folder1, "", "", "") assert.NoError(t, err) err = dataprovider.AddFolder(&folder2, "", "", "") assert.NoError(t, err) action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFolderQuotaReset, } err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, }, }, }) assert.Error(t, err) // no home dir err = os.MkdirAll(folder1.MappedPath, os.ModePerm) assert.NoError(t, err) err = os.WriteFile(filepath.Join(folder1.MappedPath, "file.txt"), []byte("folder"), 0666) assert.NoError(t, err) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, }, }, }) assert.NoError(t, err) folderGet, err := dataprovider.GetFolderByName(foldername1) assert.NoError(t, err) assert.Equal(t, 1, folderGet.UsedQuotaFiles) assert.Equal(t, int64(6), folderGet.UsedQuotaSize) // simulate another quota scan in progress assert.True(t, QuotaScans.AddVFolderQuotaScan(foldername1)) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, }, }, }) assert.Error(t, err) assert.True(t, QuotaScans.RemoveVFolderQuotaScan(foldername1)) err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no folder match", }, }, }) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no folder quota reset executed") } body, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{ Method: http.MethodPost, }, nil, nil, dataprovider.User{}, &EventParams{}, true) assert.NoError(t, err) assert.Nil(t, body) body, _, err = getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{ Method: http.MethodPost, Body: "test body", }, nil, nil, dataprovider.User{}, &EventParams{}, false) assert.NoError(t, err) assert.NotNil(t, body) err = os.RemoveAll(folder1.MappedPath) assert.NoError(t, err) err = dataprovider.DeleteFolder(foldername1, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteFolder(foldername2, "", "", "") assert.NoError(t, err) } func TestIDPAccountCheckRule(t *testing.T) { _, _, err := executeIDPAccountCheckRule(dataprovider.EventRule{}, EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no action executed") } _, _, err = executeIDPAccountCheckRule(dataprovider.EventRule{ Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: "n", Type: dataprovider.ActionTypeIDPAccountCheck, }, }, }, }, EventParams{Event: "invalid"}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unsupported IDP login event") } // invalid json _, err = executeAdminCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateAdmin: "{"}, &EventParams{Name: "missing admin"}) assert.Error(t, err) _, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateUser: "["}, &EventParams{Name: "missing user"}) assert.Error(t, err) _, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateUser: "{}"}, &EventParams{Name: "invalid user template"}) assert.ErrorIs(t, err, util.ErrValidation) username := "u" c := &dataprovider.EventActionIDPAccountCheck{ Mode: 1, TemplateUser: `{"username":"` + username + `","status":1,"home_dir":"` + util.JSONEscape(filepath.Join(os.TempDir())) + `","permissions":{"/":["*"]}}`, } params := &EventParams{ Name: username, Event: IDPLoginUser, } user, err := executeUserCheckAction(c, params) assert.NoError(t, err) assert.Equal(t, username, user.Username) assert.Equal(t, 1, user.Status) user.Status = 0 err = dataprovider.UpdateUser(user, "", "", "") assert.NoError(t, err) // the user is not changed user, err = executeUserCheckAction(c, params) assert.NoError(t, err) assert.Equal(t, username, user.Username) assert.Equal(t, 0, user.Status) // change the mode, the user is now updated c.Mode = 0 user, err = executeUserCheckAction(c, params) assert.NoError(t, err) assert.Equal(t, username, user.Username) assert.Equal(t, 1, user.Status) err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) // check rule consistency r := dataprovider.EventRule{ Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeIDPAccountCheck, }, Order: 1, }, }, } err = r.CheckActionsConsistency("") if assert.Error(t, err) { assert.Contains(t, err.Error(), "IDP account check action is only supported for IDP login trigger") } r.Trigger = dataprovider.EventTriggerIDPLogin err = r.CheckActionsConsistency("") if assert.Error(t, err) { assert.Contains(t, err.Error(), "IDP account check must be a sync action") } r.Actions[0].Options.ExecuteSync = true err = r.CheckActionsConsistency("") assert.NoError(t, err) r.Actions = append(r.Actions, dataprovider.EventAction{ BaseEventAction: dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeCommand, }, Options: dataprovider.EventActionOptions{ ExecuteSync: true, }, Order: 2, }) err = r.CheckActionsConsistency("") if assert.Error(t, err) { assert.Contains(t, err.Error(), "IDP account check must be the only sync action") } } func TestUserExpirationCheck(t *testing.T) { username := "test_user_expiration_check" user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, HomeDir: filepath.Join(os.TempDir(), username), ExpirationDate: util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour)), }, } user.Filters.PasswordExpiration = 5 err := dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) conditions := dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, } err = executeUserExpirationCheckRuleAction(conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "expired users") } // the check will be skipped, the user is expired err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{Threshold: 10}, conditions, &EventParams{}) assert.NoError(t, err) err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestEventRuleActionsNoGroupMatching(t *testing.T) { username := "test_user_action_group_matching" user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, HomeDir: filepath.Join(os.TempDir(), username), }, } err := dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) conditions := dataprovider.ConditionOptions{ GroupNames: []dataprovider.ConditionPattern{ { Pattern: "agroup", }, }, } err = executeDeleteFsRuleAction(nil, nil, conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no delete executed") } err = executeMkdirFsRuleAction(nil, nil, conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no mkdir executed") } err = executeRenameFsRuleAction(nil, nil, conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no rename executed") } err = executeExistFsRuleAction(nil, nil, conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no existence check executed") } err = executeCopyFsRuleAction(nil, nil, conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no copy executed") } err = executeUsersQuotaResetRuleAction(conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no user quota reset executed") } err = executeMetadataCheckRuleAction(conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no metadata check executed") } err = executeTransferQuotaResetRuleAction(conditions, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "no transfer quota reset executed") } err = executeDataRetentionCheckRuleAction(dataprovider.EventActionDataRetentionConfig{}, conditions, &EventParams{}, "") if assert.Error(t, err) { assert.Contains(t, err.Error(), "no retention check executed") } err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestGetFileContent(t *testing.T) { username := "test_user_get_file_content" user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, HomeDir: filepath.Join(os.TempDir(), username), }, } err := dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) fileContent := []byte("test file content") err = os.WriteFile(filepath.Join(user.GetHomeDir(), "file.txt"), fileContent, 0666) assert.NoError(t, err) conn := NewBaseConnection(xid.New().String(), protocolEventAction, "", "", user) replacer := strings.NewReplacer("old", "new") files, err := getMailAttachments(conn, []string{"/file.txt"}, replacer) assert.NoError(t, err) if assert.Len(t, files, 1) { var b bytes.Buffer _, err = files[0].Writer(&b) assert.NoError(t, err) assert.Equal(t, fileContent, b.Bytes()) } // missing file _, err = getMailAttachments(conn, []string{"/file1.txt"}, replacer) assert.Error(t, err) // directory _, err = getMailAttachments(conn, []string{"/"}, replacer) assert.Error(t, err) // files too large content := make([]byte, maxAttachmentsSize/2+1) _, err = rand.Read(content) assert.NoError(t, err) err = os.WriteFile(filepath.Join(user.GetHomeDir(), "file1.txt"), content, 0666) assert.NoError(t, err) err = os.WriteFile(filepath.Join(user.GetHomeDir(), "file2.txt"), content, 0666) assert.NoError(t, err) files, err = getMailAttachments(conn, []string{"/file1.txt"}, replacer) assert.NoError(t, err) if assert.Len(t, files, 1) { var b bytes.Buffer _, err = files[0].Writer(&b) assert.NoError(t, err) assert.Equal(t, content, b.Bytes()) } _, err = getMailAttachments(conn, []string{"/file1.txt", "/file2.txt"}, replacer) if assert.Error(t, err) { assert.Contains(t, err.Error(), "size too large") } // change the filesystem provider user.FsConfig.Provider = sdk.CryptedFilesystemProvider user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("pwd") err = dataprovider.UpdateUser(&user, "", "", "") assert.NoError(t, err) conn = NewBaseConnection(xid.New().String(), protocolEventAction, "", "", user) // the file is not encrypted so reading the encryption header will fail files, err = getMailAttachments(conn, []string{"/file.txt"}, replacer) assert.NoError(t, err) if assert.Len(t, files, 1) { var b bytes.Buffer _, err = files[0].Writer(&b) assert.Error(t, err) } err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestFilesystemActionErrors(t *testing.T) { err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unsupported filesystem action") } username := "test_user_for_actions" testReplacer := strings.NewReplacer("old", "new") user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, HomeDir: filepath.Join(os.TempDir(), username), }, FsConfig: vfs.Filesystem{ Provider: sdk.SFTPFilesystemProvider, SFTPConfig: vfs.SFTPFsConfig{ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{ Endpoint: "127.0.0.1:4022", Username: username, }, Password: kms.NewPlainSecret("pwd"), }, }, } err = executeEmailRuleAction(dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.net"}, Subject: "subject", Body: "body", Attachments: []string{"/file.txt"}, }, &EventParams{ sender: username, }) assert.Error(t, err) conn := NewBaseConnection("", protocolEventAction, "", "", user) err = executeDeleteFileFsAction(conn, "", nil) assert.Error(t, err) err = dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) // check root fs fails err = executeDeleteFsActionForUser(nil, testReplacer, user) assert.Error(t, err) err = executeMkDirsFsActionForUser(nil, testReplacer, user) assert.Error(t, err) err = executeRenameFsActionForUser(nil, testReplacer, user) assert.Error(t, err) err = executeExistFsActionForUser(nil, testReplacer, user) assert.Error(t, err) err = executeCopyFsActionForUser(nil, testReplacer, user) assert.Error(t, err) err = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, testReplacer, user) assert.Error(t, err) _, _, _, _, err = getFileWriter(conn, "/path.txt", -1) //nolint:dogsled assert.Error(t, err) err = executeEmailRuleAction(dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.net"}, Subject: "subject", Body: "body", Attachments: []string{"/file1.txt"}, }, &EventParams{ sender: username, }) assert.Error(t, err) fn := getFileContentFn(NewBaseConnection("", protocolEventAction, "", "", user), "/f.txt", 1234) var b bytes.Buffer _, err = fn(&b) assert.Error(t, err) err = executeHTTPRuleAction(dataprovider.EventActionHTTPConfig{ Endpoint: "http://127.0.0.1:9999/", Method: http.MethodPost, Parts: []dataprovider.HTTPPart{ { Name: "p1", Filepath: "/filepath", }, }, }, &EventParams{ sender: username, }) assert.Error(t, err) user.FsConfig.Provider = sdk.LocalFilesystemProvider user.Permissions["/"] = []string{dataprovider.PermUpload} err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) err = dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) err = executeRenameFsActionForUser([]dataprovider.KeyValue{ { Key: "/p1", Value: "/p1", }, }, testReplacer, user) if assert.Error(t, err) { assert.Contains(t, err.Error(), "the rename source and target cannot be the same") } err = executeRuleAction(dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFilesystem, Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionRename, Renames: []dataprovider.KeyValue{ { Key: "/p2", Value: "/p2", }, }, }, }, }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, }) assert.Error(t, err) if runtime.GOOS != osWindows { dirPath := filepath.Join(user.HomeDir, "adir", "sub") err := os.MkdirAll(dirPath, os.ModePerm) assert.NoError(t, err) filePath := filepath.Join(dirPath, "f.dat") err = os.WriteFile(filePath, []byte("test file content"), 0666) assert.NoError(t, err) err = os.Chmod(dirPath, 0001) assert.NoError(t, err) err = executeDeleteFsActionForUser([]string{"/adir/sub"}, testReplacer, user) assert.Error(t, err) err = executeDeleteFsActionForUser([]string{"/adir/sub/f.dat"}, testReplacer, user) assert.Error(t, err) err = os.Chmod(dirPath, 0555) assert.NoError(t, err) err = executeDeleteFsActionForUser([]string{"/adir/sub/f.dat"}, testReplacer, user) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unable to remove file") } err = executeRuleAction(dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFilesystem, Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionDelete, Deletes: []string{"/adir/sub/f.dat"}, }, }, }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, }) assert.Error(t, err) err = executeMkDirsFsActionForUser([]string{"/adir/sub/sub"}, testReplacer, user) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unable to create dir") } err = executeMkDirsFsActionForUser([]string{"/adir/sub/sub/sub"}, testReplacer, user) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unable to check parent dirs") } err = executeRuleAction(dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFilesystem, Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionMkdirs, MkDirs: []string{"/adir/sub/sub1"}, }, }, }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, }) assert.Error(t, err) err = os.Chmod(dirPath, os.ModePerm) assert.NoError(t, err) conn = NewBaseConnection("", protocolEventAction, "", "", user) wr := &zipWriterWrapper{ Name: "test.zip", Writer: zip.NewWriter(bytes.NewBuffer(nil)), Entries: map[string]bool{}, } err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub") assert.Error(t, err) assert.Contains(t, getErrorString(err), "is outside base dir") } err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { oldProviderConf := dataprovider.GetProviderConfig() providerConf := dataprovider.GetProviderConfig() providerConf.TrackQuota = 0 err := dataprovider.Close() assert.NoError(t, err) err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) username := "u1" user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: username, HomeDir: filepath.Join(os.TempDir(), username), Status: 1, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, }, FsConfig: vfs.Filesystem{ Provider: sdk.LocalFilesystemProvider, }, } err = dataprovider.AddUser(&user, "", "", "") assert.NoError(t, err) err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeUserQuotaReset}, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, }) assert.Error(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeTransferQuotaReset}, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, }, }, }) assert.Error(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) foldername := "f1" folder := vfs.BaseVirtualFolder{ Name: foldername, MappedPath: filepath.Join(os.TempDir(), foldername), } err = dataprovider.AddFolder(&folder, "", "", "") assert.NoError(t, err) err = os.MkdirAll(folder.MappedPath, os.ModePerm) assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeFolderQuotaReset}, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername, }, }, }) assert.Error(t, err) err = os.RemoveAll(folder.MappedPath) assert.NoError(t, err) err = dataprovider.DeleteFolder(foldername, "", "", "") assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = dataprovider.Initialize(oldProviderConf, configDir, true) assert.NoError(t, err) } func TestScheduledActions(t *testing.T) { startEventScheduler() backupsPath := filepath.Join(os.TempDir(), "backups") err := os.RemoveAll(backupsPath) assert.NoError(t, err) action := &dataprovider.BaseEventAction{ Name: "action", Type: dataprovider.ActionTypeBackup, } err = dataprovider.AddEventAction(action, "", "", "") assert.NoError(t, err) rule := &dataprovider.EventRule{ Name: "rule", Status: 1, Trigger: dataprovider.EventTriggerSchedule, Conditions: dataprovider.EventConditions{ Schedules: []dataprovider.Schedule{ { Hours: "11", DayOfWeek: "*", DayOfMonth: "*", Month: "*", }, }, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action.Name, }, Order: 1, }, }, } job := eventCronJob{ ruleName: rule.Name, } job.Run() // rule not found assert.NoDirExists(t, backupsPath) err = dataprovider.AddEventRule(rule, "", "", "") assert.NoError(t, err) job.Run() assert.DirExists(t, backupsPath) action.Type = dataprovider.ActionTypeEmail action.Options = dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"example@example.com"}, Subject: "test with attachments", Body: "body", Attachments: []string{"/file1.txt"}, }, } err = dataprovider.UpdateEventAction(action, "", "", "") assert.NoError(t, err) job.Run() // action is not compatible with a scheduled rule err = dataprovider.DeleteEventRule(rule.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventAction(action.Name, "", "", "") assert.NoError(t, err) err = os.RemoveAll(backupsPath) assert.NoError(t, err) stopEventScheduler() } func TestEventParamsCopy(t *testing.T) { params := EventParams{ Name: "name", Event: "event", Status: 1, errors: []string{"error1"}, retentionChecks: []executedRetentionCheck{}, } paramsCopy := params.getACopy() assert.Equal(t, params, *paramsCopy) params.Name = "name mod" paramsCopy.Event = "event mod" paramsCopy.Status = 2 params.errors = append(params.errors, "error2") paramsCopy.errors = append(paramsCopy.errors, "error3") assert.Equal(t, []string{"error1", "error3"}, paramsCopy.errors) assert.Equal(t, []string{"error1", "error2"}, params.errors) assert.Equal(t, "name mod", params.Name) assert.Equal(t, "name", paramsCopy.Name) assert.Equal(t, "event", params.Event) assert.Equal(t, "event mod", paramsCopy.Event) assert.Equal(t, 1, params.Status) assert.Equal(t, 2, paramsCopy.Status) params = EventParams{ retentionChecks: []executedRetentionCheck{ { Username: "u", ActionName: "a", Results: []folderRetentionCheckResult{ { Path: "p", Retention: 1, }, }, }, }, } paramsCopy = params.getACopy() require.Len(t, paramsCopy.retentionChecks, 1) paramsCopy.retentionChecks[0].Username = "u_copy" paramsCopy.retentionChecks[0].ActionName = "a_copy" require.Len(t, paramsCopy.retentionChecks[0].Results, 1) paramsCopy.retentionChecks[0].Results[0].Path = "p_copy" paramsCopy.retentionChecks[0].Results[0].Retention = 2 assert.Equal(t, "u", params.retentionChecks[0].Username) assert.Equal(t, "a", params.retentionChecks[0].ActionName) assert.Equal(t, "p", params.retentionChecks[0].Results[0].Path) assert.Equal(t, 1, params.retentionChecks[0].Results[0].Retention) assert.Equal(t, "u_copy", paramsCopy.retentionChecks[0].Username) assert.Equal(t, "a_copy", paramsCopy.retentionChecks[0].ActionName) assert.Equal(t, "p_copy", paramsCopy.retentionChecks[0].Results[0].Path) assert.Equal(t, 2, paramsCopy.retentionChecks[0].Results[0].Retention) assert.Nil(t, params.IDPCustomFields) params.addIDPCustomFields(nil) assert.Nil(t, params.IDPCustomFields) params.IDPCustomFields = &map[string]string{ "field1": "val1", } paramsCopy = params.getACopy() for k, v := range *paramsCopy.IDPCustomFields { assert.Equal(t, "field1", k) assert.Equal(t, "val1", v) } assert.Equal(t, params.IDPCustomFields, paramsCopy.IDPCustomFields) paramsCopy.addIDPCustomFields(&map[string]any{ "field2": "val2", }) assert.NotEqual(t, params.IDPCustomFields, paramsCopy.IDPCustomFields) } func TestEventParamsStatusFromError(t *testing.T) { params := EventParams{Status: 1} params.AddError(os.ErrNotExist) assert.Equal(t, 1, params.Status) params = EventParams{Status: 1, updateStatusFromError: true} params.AddError(os.ErrNotExist) assert.Equal(t, 2, params.Status) } type testWriter struct { errTest error sentinel string } func (w *testWriter) Write(p []byte) (int, error) { if w.errTest != nil { return 0, w.errTest } if w.sentinel == string(p) { return 0, io.ErrUnexpectedEOF } return len(p), nil } func TestWriteHTTPPartsError(t *testing.T) { m := multipart.NewWriter(&testWriter{ errTest: io.ErrShortWrite, }) err := writeHTTPPart(m, dataprovider.HTTPPart{}, nil, nil, nil, &EventParams{}, false) assert.ErrorIs(t, err, io.ErrShortWrite) body := "test body" m = multipart.NewWriter(&testWriter{sentinel: body}) err = writeHTTPPart(m, dataprovider.HTTPPart{ Body: body, }, nil, nil, nil, &EventParams{}, false) assert.ErrorIs(t, err, io.ErrUnexpectedEOF) } func TestReplacePathsPlaceholders(t *testing.T) { replacer := strings.NewReplacer("{{VirtualPath}}", "/path1") paths := []string{"{{VirtualPath}}", "/path1"} paths = replacePathsPlaceholders(paths, replacer) assert.Equal(t, []string{"/path1"}, paths) paths = []string{"{{VirtualPath}}", "/path2"} paths = replacePathsPlaceholders(paths, replacer) assert.Equal(t, []string{"/path1", "/path2"}, paths) } func TestEstimateZipSizeErrors(t *testing.T) { u := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: "u", HomeDir: filepath.Join(os.TempDir(), "u"), Status: 1, Permissions: map[string][]string{ "/": {dataprovider.PermAny}, }, QuotaSize: 1000, }, } err := dataprovider.AddUser(&u, "", "", "") assert.NoError(t, err) err = os.MkdirAll(u.GetHomeDir(), os.ModePerm) assert.NoError(t, err) conn := NewBaseConnection("", ProtocolFTP, "", "", u) _, err = getSizeForPath(conn, "/missing", vfs.NewFileInfo("missing", true, 0, time.Now(), false)) assert.True(t, conn.IsNotExistError(err)) if runtime.GOOS != osWindows { err = os.MkdirAll(filepath.Join(u.HomeDir, "d1", "d2", "sub"), os.ModePerm) assert.NoError(t, err) err = os.WriteFile(filepath.Join(u.HomeDir, "d1", "d2", "sub", "file.txt"), []byte("data"), 0666) assert.NoError(t, err) err = os.Chmod(filepath.Join(u.HomeDir, "d1", "d2"), 0001) assert.NoError(t, err) size, err := estimateZipSize(conn, "/archive.zip", []string{"/d1"}) assert.Error(t, err, "size %d", size) err = os.Chmod(filepath.Join(u.HomeDir, "d1", "d2"), os.ModePerm) assert.NoError(t, err) } err = dataprovider.DeleteUser(u.Username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(u.GetHomeDir()) assert.NoError(t, err) } func TestOnDemandRule(t *testing.T) { a := &dataprovider.BaseEventAction{ Name: "a", Type: dataprovider.ActionTypeBackup, Options: dataprovider.BaseEventActionOptions{}, } err := dataprovider.AddEventAction(a, "", "", "") assert.NoError(t, err) r := &dataprovider.EventRule{ Name: "test on demand rule", Status: 1, Trigger: dataprovider.EventTriggerOnDemand, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: a.Name, }, }, }, } err = dataprovider.AddEventRule(r, "", "", "") assert.NoError(t, err) err = RunOnDemandRule(r.Name) assert.NoError(t, err) r.Status = 0 err = dataprovider.UpdateEventRule(r, "", "", "") assert.NoError(t, err) err = RunOnDemandRule(r.Name) assert.ErrorIs(t, err, util.ErrValidation) assert.Contains(t, err.Error(), "is inactive") r.Status = 1 r.Trigger = dataprovider.EventTriggerCertificate err = dataprovider.UpdateEventRule(r, "", "", "") assert.NoError(t, err) err = RunOnDemandRule(r.Name) assert.ErrorIs(t, err, util.ErrValidation) assert.Contains(t, err.Error(), "is not defined as on-demand") a1 := &dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"example@example.org"}, Subject: "subject", Body: "body", Attachments: []string{"/{{VirtualPath}}"}, }, }, } err = dataprovider.AddEventAction(a1, "", "", "") assert.NoError(t, err) r.Trigger = dataprovider.EventTriggerOnDemand r.Actions = []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: a1.Name, }, }, } err = dataprovider.UpdateEventRule(r, "", "", "") assert.NoError(t, err) err = RunOnDemandRule(r.Name) assert.ErrorIs(t, err, util.ErrValidation) assert.Contains(t, err.Error(), "incosistent actions") err = dataprovider.DeleteEventRule(r.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventAction(a.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventAction(a1.Name, "", "", "") assert.NoError(t, err) err = RunOnDemandRule(r.Name) assert.ErrorIs(t, err, util.ErrNotFound) } func getErrorString(err error) string { if err == nil { return "" } return err.Error() } func TestHTTPEndpointWithPlaceholders(t *testing.T) { c := dataprovider.EventActionHTTPConfig{ Endpoint: "http://127.0.0.1:8080/base/url/{{Name}}/{{VirtualPath}}/upload", QueryParameters: []dataprovider.KeyValue{ { Key: "u", Value: "{{Name}}", }, { Key: "p", Value: "{{VirtualPath}}", }, }, } name := "uname" vPath := "/a dir/@ file.txt" replacer := strings.NewReplacer("{{Name}}", name, "{{VirtualPath}}", vPath) u, err := getHTTPRuleActionEndpoint(c, replacer) assert.NoError(t, err) expected := "http://127.0.0.1:8080/base/url/" + url.PathEscape(name) + "/" + url.PathEscape(vPath) + "/upload?" + "p=" + url.QueryEscape(vPath) + "&u=" + url.QueryEscape(name) assert.Equal(t, expected, u) c.Endpoint = "http://127.0.0.1/upload" u, err = getHTTPRuleActionEndpoint(c, replacer) assert.NoError(t, err) expected = c.Endpoint + "?p=" + url.QueryEscape(vPath) + "&u=" + url.QueryEscape(name) assert.Equal(t, expected, u) }