From eeef23139df081b9b3d2cfe8f2d88dd1264c9686 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 23 Sep 2024 19:55:03 +0200 Subject: [PATCH] EventManager: filter action execution based on event status Signed-off-by: Nicola Murino --- go.mod | 4 +- go.sum | 8 +-- internal/common/eventmanager.go | 5 ++ internal/common/protocol_test.go | 102 +++++++++++++++++++++++++++++ internal/dataprovider/eventrule.go | 22 +++++++ internal/httpd/httpd_test.go | 18 ++++- internal/httpd/webadmin.go | 8 +++ internal/httpdtest/httpdtest.go | 10 ++- static/locales/en/translation.json | 1 + static/locales/it/translation.json | 1 + templates/webadmin/eventrule.html | 12 ++++ 11 files changed, 183 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 81daf17f..6191a49e 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 github.com/jackc/pgx/v5 v5.7.1 github.com/jlaffaye/ftp v0.2.0 - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.17.10 github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/mattn/go-sqlite3 v1.14.23 @@ -81,7 +81,7 @@ require ( cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect - cloud.google.com/go/compute/metadata v0.5.1 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect cloud.google.com/go/iam v1.2.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect diff --git a/go.sum b/go.sum index 6493f5e3..3046cc68 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ cloud.google.com/go/auth v0.9.4 h1:DxF7imbEbiFu9+zdKC6cKBko1e8XeJnipNqIbWZ+kDI= cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= -cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= -cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/kms v1.19.0 h1:x0OVJDl6UH1BSX4THKlMfdcFWoE4ruh90ZHuilZekrU= @@ -241,8 +241,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index 6b715d7c..ccb6b607 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -2597,6 +2597,11 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params * func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, //nolint:gocyclo conditions dataprovider.ConditionOptions, ) error { + if len(conditions.EventStatuses) > 0 && !slices.Contains(conditions.EventStatuses, params.Status) { + eventManagerLog(logger.LevelDebug, "skipping action %s, event status %d does not match: %v", + action.Name, params.Status, conditions.EventStatuses) + return nil + } var err error switch action.Type { diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 3fa2062b..1e0fcb7e 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -3815,6 +3815,7 @@ func TestEventRule(t *testing.T) { Conditions: dataprovider.EventConditions{ FsEvents: []string{"upload"}, Options: dataprovider.ConditionOptions{ + EventStatuses: []int{1}, FsPaths: []dataprovider.ConditionPattern{ { Pattern: "/subdir/*.dat", @@ -4105,6 +4106,107 @@ func TestEventRule(t *testing.T) { require.NoError(t, err) } +func TestEventRuleStatues(t *testing.T) { + smtpCfg := smtp.Config{ + Host: "127.0.0.1", + Port: 2525, + From: "notification@example.com", + TemplatesPath: "templates", + } + err := smtpCfg.Initialize(configDir, true) + require.NoError(t, err) + + a1 := dataprovider.BaseEventAction{ + Name: "a1", + Type: dataprovider.ActionTypeEmail, + Options: dataprovider.BaseEventActionOptions{ + EmailConfig: dataprovider.EventActionEmailConfig{ + Recipients: []string{"test6@example.com"}, + Subject: `New "{{Event}}" error`, + Body: "{{ErrorString}}", + }, + }, + } + action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) + assert.NoError(t, err) + + r := dataprovider.EventRule{ + Name: "rule", + Status: 1, + Trigger: dataprovider.EventTriggerFsEvent, + Conditions: dataprovider.EventConditions{ + FsEvents: []string{"upload"}, + Options: dataprovider.ConditionOptions{ + EventStatuses: []int{3}, + }, + }, + Actions: []dataprovider.EventAction{ + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: action1.Name, + }, + Order: 1, + }, + }, + } + rule, resp, err := httpdtest.AddEventRule(r, http.StatusCreated) + assert.NoError(t, err, string(resp)) + + u := getTestUser() + u.UploadDataTransfer = 1 + u.DownloadDataTransfer = 1 + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + conn, client, err := getSftpClient(user) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + + testFileSize := int64(999999) + err = writeSFTPFile(testFileName, testFileSize, client) + assert.NoError(t, err) + f, err := client.Open(testFileName) + assert.NoError(t, err) + contents := make([]byte, testFileSize) + n, err := io.ReadFull(f, contents) + assert.NoError(t, err) + assert.Equal(t, int(testFileSize), n) + assert.Len(t, contents, int(testFileSize)) + err = f.Close() + assert.NoError(t, err) + + lastReceivedEmail.reset() + assert.Eventually(t, func() bool { + return lastReceivedEmail.get().From == "" + }, 600*time.Millisecond, 500*time.Millisecond) + + err = writeSFTPFile(testFileName, testFileSize, client) + assert.Error(t, err) + lastReceivedEmail.reset() + assert.Eventually(t, func() bool { + return lastReceivedEmail.get().From != "" + }, 3000*time.Millisecond, 100*time.Millisecond) + email := lastReceivedEmail.get() + assert.Len(t, email.To, 1) + assert.True(t, slices.Contains(email.To, "test6@example.com")) + assert.Contains(t, email.Data, `Subject: New "upload" error`) + assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error()) + } + + _, err = httpdtest.RemoveEventRule(rule, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) + + smtpCfg = smtp.Config{} + err = smtpCfg.Initialize(configDir, true) + require.NoError(t, err) +} + func TestEventRuleProviderEvents(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go index 52d14262..6f067765 100644 --- a/internal/dataprovider/eventrule.go +++ b/internal/dataprovider/eventrule.go @@ -1336,6 +1336,7 @@ type ConditionOptions struct { ProviderObjects []string `json:"provider_objects,omitempty"` MinFileSize int64 `json:"min_size,omitempty"` MaxFileSize int64 `json:"max_size,omitempty"` + EventStatuses []int `json:"event_statuses,omitempty"` // allow to execute scheduled tasks concurrently from multiple instances ConcurrentExecution bool `json:"concurrent_execution,omitempty"` } @@ -1345,6 +1346,8 @@ func (f *ConditionOptions) getACopy() ConditionOptions { copy(protocols, f.Protocols) providerObjects := make([]string, len(f.ProviderObjects)) copy(providerObjects, f.ProviderObjects) + statuses := make([]int, len(f.EventStatuses)) + copy(statuses, f.EventStatuses) return ConditionOptions{ Names: cloneConditionPatterns(f.Names), @@ -1355,10 +1358,20 @@ func (f *ConditionOptions) getACopy() ConditionOptions { ProviderObjects: providerObjects, MinFileSize: f.MinFileSize, MaxFileSize: f.MaxFileSize, + EventStatuses: statuses, ConcurrentExecution: f.ConcurrentExecution, } } +func (f *ConditionOptions) validateStatuses() error { + for _, status := range f.EventStatuses { + if status < 0 || status > 3 { + return util.NewValidationError(fmt.Sprintf("invalid event_status %d", status)) + } + } + return nil +} + func (f *ConditionOptions) validate() error { if err := validateConditionPatterns(f.Names); err != nil { return err @@ -1389,6 +1402,9 @@ func (f *ConditionOptions) validate() error { util.ByteCountSI(f.MaxFileSize), util.ByteCountSI(f.MinFileSize))) } } + if err := f.validateStatuses(); err != nil { + return err + } if config.IsShared == 0 { f.ConcurrentExecution = false } @@ -1491,6 +1507,7 @@ func (c *EventConditions) validate(trigger int) error { c.Options.GroupNames = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.IDPLoginEvent = 0 @@ -1510,6 +1527,7 @@ func (c *EventConditions) validate(trigger int) error { c.ProviderEvents = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.Options.ProviderObjects = nil @@ -1525,6 +1543,7 @@ func (c *EventConditions) validate(trigger int) error { c.Options.RoleNames = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.Schedules = nil @@ -1534,6 +1553,7 @@ func (c *EventConditions) validate(trigger int) error { c.ProviderEvents = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.Options.ProviderObjects = nil @@ -1547,6 +1567,7 @@ func (c *EventConditions) validate(trigger int) error { c.Options.RoleNames = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.Schedules = nil @@ -1560,6 +1581,7 @@ func (c *EventConditions) validate(trigger int) error { c.Options.RoleNames = nil c.Options.FsPaths = nil c.Options.Protocols = nil + c.Options.EventStatuses = nil c.Options.MinFileSize = 0 c.Options.MaxFileSize = 0 c.Schedules = nil diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index beb290b4..3670d396 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -1914,7 +1914,8 @@ func TestBasicActionRulesHandling(t *testing.T) { Conditions: dataprovider.EventConditions{ FsEvents: []string{"upload"}, Options: dataprovider.ConditionOptions{ - MinFileSize: 1024 * 1024, + EventStatuses: []int{2, 3}, + MinFileSize: 1024 * 1024, }, }, Actions: []dataprovider.EventAction{ @@ -2708,6 +2709,21 @@ func TestEventRuleValidation(t *testing.T) { _, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest) assert.NoError(t, err) assert.Contains(t, string(resp), "sync execution is only supported for upload and pre-* events") + + rule.Conditions.FsEvents = []string{"download"} + rule.Conditions.Options.EventStatuses = []int{3, 2, 8} + rule.Actions = []dataprovider.EventAction{ + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: "action", + }, + Order: 1, + }, + } + _, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest) + assert.NoError(t, err) + assert.Contains(t, string(resp), "invalid event_status") + rule.Trigger = dataprovider.EventTriggerProviderEvent rule.Actions = []dataprovider.EventAction{ { diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 5f8ce01d..7feff43b 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -2555,6 +2555,13 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo if err != nil { return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid max file size: %w", err), util.I18nErrorInvalidMaxSize) } + var eventStatuses []int + for _, s := range r.Form["fs_statuses"] { + status, err := strconv.ParseInt(s, 10, 32) + if err == nil { + eventStatuses = append(eventStatuses, int(status)) + } + } conditions := dataprovider.EventConditions{ FsEvents: r.Form["fs_events"], ProviderEvents: r.Form["provider_events"], @@ -2566,6 +2573,7 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo RoleNames: roleNames, FsPaths: fsPaths, Protocols: r.Form["fs_protocols"], + EventStatuses: eventStatuses, ProviderObjects: r.Form["provider_objects"], MinFileSize: minFileSize, MaxFileSize: maxFileSize, diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index f6ec7aed..68d0a8b5 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -1662,7 +1662,7 @@ func compareConditionPatternOptions(expected, actual []dataprovider.ConditionPat return nil } -func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions) error { +func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions) error { //nolint:gocyclo if err := compareConditionPatternOptions(expected.Names, actual.Names); err != nil { return errors.New("condition names mismatch") } @@ -1683,6 +1683,14 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions) return errors.New("condition protocols content mismatch") } } + if len(expected.EventStatuses) != len(actual.EventStatuses) { + return errors.New("condition statuses mismatch") + } + for _, v := range expected.EventStatuses { + if !slices.Contains(actual.EventStatuses, v) { + return errors.New("condition statuses content mismatch") + } + } if len(expected.ProviderObjects) != len(actual.ProviderObjects) { return errors.New("condition provider objects mismatch") } diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 1bf8614d..0fcc5e11 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -1090,6 +1090,7 @@ "scheduler_help": "Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on", "concurrent_run": "Allow concurrent execution from multiple instances", "protocol_filters": "Protocol filters", + "status_filters": "Status filters", "object_filters": "Object filters", "name_filters": "Name filters", "name_filters_help": "Shell-like pattern filters for usernames, folder names. For example \"user*\"\" will match names starting with \"user\". For provider events, this filter is applied to the username of the admin executing the event", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index 39dfd927..7b315c94 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -1090,6 +1090,7 @@ "scheduler_help": "Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via", "concurrent_run": "Consentire l'esecuzione simultanea da più istanze", "protocol_filters": "Filtro su protocolli", + "status_filters": "Filtro su stati", "object_filters": "Filtro su oggetti", "name_filters": "Filtro su nomi", "name_filters_help": "Filtri per nomi utente e nomi di cartelle. Ad esempio, \"user*\"\" corrisponderà per i nomi che iniziano con \"user\". Per gli eventi del provider, questo filtro viene applicato al nome utente dell'amministratore che esegue l'evento", diff --git a/templates/webadmin/eventrule.html b/templates/webadmin/eventrule.html index 4a2b1356..76c52eab 100644 --- a/templates/webadmin/eventrule.html +++ b/templates/webadmin/eventrule.html @@ -201,6 +201,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). +
+ +
+ +
+
+
+