From 56bf51277c59b0a07890ffbcb14a088f11942707 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 29 Aug 2022 19:03:31 +0200 Subject: [PATCH] eventmanager placeholders: add StatusString and ErrorString Signed-off-by: Nicola Murino --- docs/eventmanager.md | 4 +- go.mod | 4 +- go.sum | 9 +- internal/acme/acme.go | 4 +- internal/common/actions.go | 8 +- internal/common/eventmanager.go | 184 +++++++++++++++++---------- internal/common/eventmanager_test.go | 118 +++++++++++------ internal/common/protocol_test.go | 98 +++++++++----- templates/webadmin/eventaction.html | 6 + 9 files changed, 285 insertions(+), 150 deletions(-) diff --git a/docs/eventmanager.md b/docs/eventmanager.md index 3f86439fbed8b2388990db41ff34d3ee23f7f98c..8ab6458eda360788386dfb75cb7fde3581696f7a 100644 --- a/docs/eventmanager.md +++ b/docs/eventmanager.md @@ -23,6 +23,8 @@ The following placeholders are supported: - `{{Name}}`. Username, folder name or admin username for provider actions. - `{{Event}}`. Event name, for example `upload`, `download` for filesystem events or `add`, `update` for provider events. - `{{Status}}`. Status for `upload`, `download` and `ssh_cmd` events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error. +- `{{StatusString}}`. Status as string. Possible values "OK", "KO". +- `{{ErrorString}}`. Error details. Replaced with an empty string if no errors occur. - `{{VirtualPath}}`. Path seen by SFTPGo users, for example `/adir/afile.txt`. - `{{FsPath}}`. Full filesystem path, for example `/user/homedir/adir/afile.txt` or `C:/data/user/homedir/adir/afile.txt` on Windows. - `{{ObjectName}}`. File/directory name, for example `afile.txt` or provider object name. @@ -51,7 +53,7 @@ Actions such as user quota reset, transfer quota reset, data retention check, fo Actions are executed in a sequential order except for sync actions that are executed before the others. For each action associated to a rule you can define the following settings: - `Stop on failure`, the next action will not be executed if the current one fails. -- `Failure action`, this action will be executed only if at least another one fails. +- `Failure action`, this action will be executed only if at least another one fails. :warning: Please note that a failure action isn't executed if the event fails, for example if a download fails the main action is executed. The failure action is executed only if one of the non-failure actions associated to a rule fails. - `Execute sync`, for upload events, you can execute the action synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection. If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions. diff --git a/go.mod b/go.mod index d571be8472fa566fb47c93788abe1cd46c51952e..63c06e2241746ea59b9c5b7aa17a693daede0788 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5 github.com/rs/xid v1.4.0 - github.com/rs/zerolog v1.27.0 + github.com/rs/zerolog v1.28.0 github.com/sftpgo/sdk v0.1.2-0.20220828084006-f9e2fffac657 github.com/shirou/gopsutil/v3 v3.22.7 github.com/spf13/afero v1.9.2 @@ -155,7 +155,7 @@ require ( golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect + google.golang.org/genproto v0.0.0-20220829144015-23454907ede3 // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index a259ef9709bfc4311dbe2d02f7443f5130732173..0d3d5a3d7c8d49de3da5e8609f9fc70878422a6d 100644 --- a/go.sum +++ b/go.sum @@ -701,13 +701,12 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5 h1:7PcjxKTsfGXpTMiTNNa1VllbsYSZJN5nhvVEWQMdX8Y= github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1221,8 +1220,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3 h1:4wwmycAWg7WUIFWgzxP6Wumy2GBLxmATgkhgpFnJl2U= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/acme/acme.go b/internal/acme/acme.go index c17c3d08f52d0c74d7a41f458943f3ffdae43e0c..54176b3ab500b4dedf5a3da52b882ada5abe1160 100644 --- a/internal/acme/acme.go +++ b/internal/acme/acme.go @@ -568,14 +568,14 @@ func (c *Configuration) notifyCertificateRenewal(domain string, err error) { } params := common.EventParams{ Name: domain, + Event: "Certificate renewal", Timestamp: time.Now().UnixNano(), } if err != nil { params.Status = 2 - params.Event = "Certificate renewal failed" + params.AddError(err) } else { params.Status = 1 - params.Event = "Successful certificate renewal" } common.HandleCertificateEvent(params) } diff --git a/internal/common/actions.go b/internal/common/actions.go index e7b0e8113ad8243ea539c419574af71b7f21046b..449cbd48fb4551eb03df3f6f472654649d6f69cf 100644 --- a/internal/common/actions.go +++ b/internal/common/actions.go @@ -122,7 +122,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua } var errRes error if hasRules { - errRes = eventManager.handleFsEvent(EventParams{ + params := EventParams{ Name: notification.Username, Event: notification.Action, Status: notification.Status, @@ -136,7 +136,11 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua IP: notification.IP, Timestamp: notification.Timestamp, Object: nil, - }) + } + if err != nil { + params.AddError(fmt.Errorf("%q failed: %w", params.Event, err)) + } + errRes = eventManager.handleFsEvent(params) } if hasHook { if util.Contains(Config.Actions.ExecuteSync, operation) { diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index ab736c40c8141d92e383cff1401eb648466d4cf7..7e1cb2ce42e30004b61931fa30d79c862bcc2680 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -406,21 +406,50 @@ func (r *eventRulesContainer) handleCertificateEvent(params EventParams) { // EventParams defines the supported event parameters type EventParams struct { - Name string - Event string - Status int - VirtualPath string - FsPath string - VirtualTargetPath string - FsTargetPath string - ObjectName string - ObjectType string - FileSize int64 - Protocol string - IP string - Timestamp int64 - Object plugin.Renderer - sender string + Name string + Event string + Status int + VirtualPath string + FsPath string + VirtualTargetPath string + FsTargetPath string + ObjectName string + ObjectType string + FileSize int64 + Protocol string + IP string + Timestamp int64 + Object plugin.Renderer + sender string + updateStatusFromError bool + errors []string +} + +func (p *EventParams) getACopy() *EventParams { + params := *p + params.errors = make([]string, len(p.errors)) + copy(params.errors, p.errors) + return ¶ms +} + +// AddError adds a new error to the event params and update the status if needed +func (p *EventParams) AddError(err error) { + if err == nil { + return + } + if p.updateStatusFromError && p.Status == 1 { + p.Status = 2 + } + p.errors = append(p.errors, err.Error()) +} + +func (p *EventParams) getStatusString() string { + switch p.Status { + case 1: + return "OK" + default: + return "KO" + } } // getUsers returns users with group settings not applied @@ -469,11 +498,18 @@ func (p *EventParams) getStringReplacements(addObjectData bool) []string { "{{Protocol}}", p.Protocol, "{{IP}}", p.IP, "{{Timestamp}}", fmt.Sprintf("%d", p.Timestamp), + "{{StatusString}}", p.getStatusString(), } + if len(p.errors) > 0 { + replacements = append(replacements, "{{ErrorString}}", strings.Join(p.errors, ", ")) + } else { + replacements = append(replacements, "{{ErrorString}}", "") + } + replacements = append(replacements, "{{ObjectData}}", "") if addObjectData { data, err := p.Object.RenderAsJSON(p.Event != operationDelete) if err == nil { - replacements = append(replacements, "{{ObjectData}}", string(data)) + replacements[len(replacements)-1] = string(data) } } return replacements @@ -516,7 +552,7 @@ func getMailAttachments(user dataprovider.User, attachments []string, replacer * err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return nil, err + return nil, fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) totalSize := int64(0) @@ -596,7 +632,7 @@ func getHTTPRuleActionEndpoint(c dataprovider.EventActionHTTPConfig, replacer *s return c.Endpoint, nil } -func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params EventParams) error { +func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventParams) error { if !c.Password.IsEmpty() { if err := c.Password.TryDecrypt(); err != nil { return fmt.Errorf("unable to decrypt password: %w", err) @@ -653,7 +689,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params EventPar return nil } -func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params EventParams) error { +func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error { envVars := make([]string, 0, len(c.EnvVars)) addObjectData := false if params.Object != nil { @@ -686,7 +722,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params Ev return err } -func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params EventParams) error { +func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *EventParams) error { addObjectData := false if params.Object != nil { if strings.Contains(c.Body, "{{ObjectData}}") { @@ -748,7 +784,7 @@ func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("delete error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range deletes { @@ -775,7 +811,7 @@ func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, } func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -792,6 +828,7 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer, } executed++ if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil { + params.AddError(err) failures = append(failures, user.Username) continue } @@ -815,7 +852,7 @@ func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, use err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("mkdir error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range dirs { @@ -832,7 +869,7 @@ func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, use } func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -874,7 +911,7 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("rename error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range renames { @@ -899,7 +936,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer, err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("existence check error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range exist { @@ -913,7 +950,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer, } func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -931,6 +968,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string executed++ if err = executeRenameFsActionForUser(renames, replacer, user); err != nil { failures = append(failures, user.Username) + params.AddError(err) continue } } @@ -945,7 +983,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string } func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, conditions dataprovider.ConditionOptions, - params EventParams, + params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -963,6 +1001,7 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit executed++ if err = executeExistFsActionForUser(exist, replacer, user); err != nil { failures = append(failures, user.Username) + params.AddError(err) continue } } @@ -977,7 +1016,7 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit } func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions, - params EventParams, + params *EventParams, ) error { addObjectData := false replacements := params.getStringReplacements(addObjectData) @@ -1003,25 +1042,25 @@ func executeQuotaResetForUser(user dataprovider.User) error { return err } if !QuotaScans.AddUserQuotaScan(user.Username) { - eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %s", user.Username) - return fmt.Errorf("another quota scan is in progress for user %s", user.Username) + eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %q", user.Username) + return fmt.Errorf("another quota scan is in progress for user %q", user.Username) } defer QuotaScans.RemoveUserQuotaScan(user.Username) numFiles, size, err := user.ScanQuota() if err != nil { - eventManagerLog(logger.LevelError, "error scanning quota for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error scanning quota for user %q: %v", user.Username, err) + return fmt.Errorf("error scanning quota for user %q: %w", user.Username, err) } err = dataprovider.UpdateUserQuota(&user, numFiles, size, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating quota for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error updating quota for user %q: %v", user.Username, err) + return fmt.Errorf("error updating quota for user %q: %w", user.Username, err) } return nil } -func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { users, err := params.getUsers() if err != nil { return fmt.Errorf("unable to get users: %w", err) @@ -1031,12 +1070,13 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, for _, user := range users { // if sender is set, the conditions have already been evaluated if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) { - eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, name conditions don't match", + eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, name conditions don't match", user.Username) continue } executed++ if err = executeQuotaResetForUser(user); err != nil { + params.AddError(err) failedResets = append(failedResets, user.Username) continue } @@ -1051,7 +1091,7 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, return nil } -func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { folders, err := params.getFolders() if err != nil { return fmt.Errorf("unable to get folders: %w", err) @@ -1066,7 +1106,8 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions continue } if !QuotaScans.AddVFolderQuotaScan(folder.Name) { - eventManagerLog(logger.LevelError, "another quota scan is already in progress for folder %s", folder.Name) + eventManagerLog(logger.LevelError, "another quota scan is already in progress for folder %q", folder.Name) + params.AddError(fmt.Errorf("another quota scan is already in progress for folder %q", folder.Name)) failedResets = append(failedResets, folder.Name) continue } @@ -1078,13 +1119,15 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions numFiles, size, err := f.ScanQuota() QuotaScans.RemoveVFolderQuotaScan(folder.Name) if err != nil { - eventManagerLog(logger.LevelError, "error scanning quota for folder %s: %v", folder.Name, err) + eventManagerLog(logger.LevelError, "error scanning quota for folder %q: %v", folder.Name, err) + params.AddError(fmt.Errorf("error scanning quota for folder %q: %w", folder.Name, err)) failedResets = append(failedResets, folder.Name) continue } err = dataprovider.UpdateVirtualFolderQuota(&folder, numFiles, size, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating quota for folder %s: %v", folder.Name, err) + eventManagerLog(logger.LevelError, "error updating quota for folder %q: %v", folder.Name, err) + params.AddError(fmt.Errorf("error updating quota for folder %q: %w", folder.Name, err)) failedResets = append(failedResets, folder.Name) } } @@ -1098,7 +1141,7 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions return nil } -func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { users, err := params.getUsers() if err != nil { return fmt.Errorf("unable to get users: %w", err) @@ -1115,7 +1158,8 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption executed++ err = dataprovider.UpdateUserTransferQuota(&user, 0, 0, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating transfer quota for user %s: %v", user.Username, err) + eventManagerLog(logger.LevelError, "error updating transfer quota for user %q: %v", user.Username, err) + params.AddError(fmt.Errorf("error updating transfer quota for user %q: %w", user.Username, err)) failedResets = append(failedResets, user.Username) } } @@ -1140,18 +1184,18 @@ func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprov } c := RetentionChecks.Add(check, &user) if c == nil { - eventManagerLog(logger.LevelError, "another retention check is already in progress for user %s", user.Username) - return fmt.Errorf("another retention check is in progress for user %s", user.Username) + eventManagerLog(logger.LevelError, "another retention check is already in progress for user %q", user.Username) + return fmt.Errorf("another retention check is in progress for user %q", user.Username) } if err := c.Start(); err != nil { - eventManagerLog(logger.LevelError, "error checking retention for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error checking retention for user %q: %v", user.Username, err) + return fmt.Errorf("error checking retention for user %q: %w", user.Username, err) } return nil } func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRetentionConfig, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -1169,6 +1213,7 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete executed++ if err = executeDataRetentionCheckForUser(user, config.Folders); err != nil { failedChecks = append(failedChecks, user.Username) + params.AddError(err) continue } } @@ -1182,29 +1227,37 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete return nil } -func executeRuleAction(action dataprovider.BaseEventAction, params EventParams, conditions dataprovider.ConditionOptions) error { +func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, conditions dataprovider.ConditionOptions) error { + var err error + switch action.Type { case dataprovider.ActionTypeHTTP: - return executeHTTPRuleAction(action.Options.HTTPConfig, params) + err = executeHTTPRuleAction(action.Options.HTTPConfig, params) case dataprovider.ActionTypeCommand: - return executeCommandRuleAction(action.Options.CmdConfig, params) + err = executeCommandRuleAction(action.Options.CmdConfig, params) case dataprovider.ActionTypeEmail: - return executeEmailRuleAction(action.Options.EmailConfig, params) + err = executeEmailRuleAction(action.Options.EmailConfig, params) case dataprovider.ActionTypeBackup: - return dataprovider.ExecuteBackup() + err = dataprovider.ExecuteBackup() case dataprovider.ActionTypeUserQuotaReset: - return executeUsersQuotaResetRuleAction(conditions, params) + err = executeUsersQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeFolderQuotaReset: - return executeFoldersQuotaResetRuleAction(conditions, params) + err = executeFoldersQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeTransferQuotaReset: - return executeTransferQuotaResetRuleAction(conditions, params) + err = executeTransferQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeDataRetentionCheck: - return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params) + err = executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params) case dataprovider.ActionTypeFilesystem: - return executeFsRuleAction(action.Options.FsConfig, conditions, params) + err = executeFsRuleAction(action.Options.FsConfig, conditions, params) default: - return fmt.Errorf("unsupported action type: %d", action.Type) + err = fmt.Errorf("unsupported action type: %d", action.Type) + } + + if err != nil { + err = fmt.Errorf("action %q failed: %w", action.Name, err) } + params.AddError(err) + return err } func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) error { @@ -1212,10 +1265,11 @@ func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) for _, rule := range rules { var failedActions []string + paramsCopy := params.getACopy() for _, action := range rule.Actions { if !action.Options.IsFailureAction && action.Options.ExecuteSync { startTime := time.Now() - if err := executeRuleAction(action.BaseEventAction, params, rule.Conditions.Options); err != nil { + if err := executeRuleAction(action.BaseEventAction, paramsCopy, rule.Conditions.Options); err != nil { eventManagerLog(logger.LevelError, "unable to execute sync action %q for rule %q, elapsed %s, err: %v", action.Name, rule.Name, time.Since(startTime), err) failedActions = append(failedActions, action.Name) @@ -1231,7 +1285,7 @@ func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) } } // execute async actions if any, including failure actions - go executeRuleAsyncActions(rule, params, failedActions) + go executeRuleAsyncActions(rule, paramsCopy, failedActions) } return errRes @@ -1242,11 +1296,11 @@ func executeAsyncRulesActions(rules []dataprovider.EventRule, params EventParams defer eventManager.removeAsyncTask() for _, rule := range rules { - executeRuleAsyncActions(rule, params, nil) + executeRuleAsyncActions(rule, params.getACopy(), nil) } } -func executeRuleAsyncActions(rule dataprovider.EventRule, params EventParams, failedActions []string) { +func executeRuleAsyncActions(rule dataprovider.EventRule, params *EventParams, failedActions []string) { for _, action := range rule.Actions { if !action.Options.IsFailureAction && !action.Options.ExecuteSync { startTime := time.Now() @@ -1361,9 +1415,9 @@ func (j *eventCronJob) Run() { } }(task.Name) - executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{}) + executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true}) } else { - executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{}) + executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true}) } eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName) } diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index 4e7c019fca47e591e65c1bfba2d1310dd6dabf58..f065f3846412238a04a57144ea53bc259ce42dbe 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -270,19 +270,19 @@ func TestEventManagerErrors(t *testing.T) { _, err = params.getFolders() assert.Error(t, err) - err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) groupName := "agroup" @@ -362,7 +362,7 @@ func TestEventManagerErrors(t *testing.T) { }, }, } - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "username1", @@ -421,10 +421,10 @@ func TestEventRuleActions(t *testing.T) { Name: actionName, Type: dataprovider.ActionTypeBackup, } - err := executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + err := executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.NoError(t, err) action.Type = -1 - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.Error(t, err) action = dataprovider.BaseEventAction{ @@ -454,12 +454,12 @@ func TestEventRuleActions(t *testing.T) { }, } action.Options.SetEmptySecretsIfNil() - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + 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{ + params := &EventParams{ Name: "a", Object: &dataprovider.User{ BaseUser: sdk.BaseUser{ @@ -472,7 +472,7 @@ func TestEventRuleActions(t *testing.T) { action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v/404", httpAddr) err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) if assert.Error(t, err) { - assert.Equal(t, err.Error(), "unexpected status code: 404") + assert.Contains(t, err.Error(), "unexpected status code: 404") } action.Options.HTTPConfig.Endpoint = "http://invalid:1234" err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) @@ -517,7 +517,7 @@ func TestEventRuleActions(t *testing.T) { action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeUserQuotaReset, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -530,7 +530,7 @@ func TestEventRuleActions(t *testing.T) { 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{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -544,7 +544,7 @@ func TestEventRuleActions(t *testing.T) { 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{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -554,7 +554,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) assert.True(t, QuotaScans.RemoveUserQuotaScan(username1)) // non matching pattern - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "don't match", @@ -578,7 +578,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -622,7 +622,7 @@ func TestEventRuleActions(t *testing.T) { err = os.Chtimes(file4, timeBeforeRetention, timeBeforeRetention) assert.NoError(t, err) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -637,7 +637,7 @@ func TestEventRuleActions(t *testing.T) { // simulate another check in progress c := RetentionChecks.Add(RetentionCheck{}, &user1) assert.NotNil(t, c) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -647,7 +647,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) RetentionChecks.remove(user1.Username) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -667,7 +667,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -677,7 +677,7 @@ func TestEventRuleActions(t *testing.T) { if assert.Error(t, err) { assert.Contains(t, err.Error(), "no existence check executed") } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -686,7 +686,7 @@ func TestEventRuleActions(t *testing.T) { }) assert.NoError(t, err) action.Options.FsConfig.Exist = []string{"/file1.txt", path.Join("/", retentionDir, "file2.txt")} - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -702,7 +702,7 @@ func TestEventRuleActions(t *testing.T) { assert.NoError(t, err) action.Type = dataprovider.ActionTypeTransferQuotaReset - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -715,7 +715,7 @@ func TestEventRuleActions(t *testing.T) { assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer) assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -737,7 +737,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -753,7 +753,7 @@ func TestEventRuleActions(t *testing.T) { Deletes: []string{"/dir1"}, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -769,7 +769,7 @@ func TestEventRuleActions(t *testing.T) { Deletes: []string{"/dir1"}, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -802,7 +802,7 @@ func TestEventRuleActions(t *testing.T) { action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFolderQuotaReset, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -814,7 +814,7 @@ func TestEventRuleActions(t *testing.T) { 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{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -828,7 +828,7 @@ func TestEventRuleActions(t *testing.T) { 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{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -838,7 +838,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) assert.True(t, QuotaScans.RemoveVFolderQuotaScan(foldername1)) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no folder match", @@ -920,7 +920,7 @@ func TestGetFileContent(t *testing.T) { } func TestFilesystemActionErrors(t *testing.T) { - err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, EventParams{}) + err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unsupported filesystem action") } @@ -950,7 +950,7 @@ func TestFilesystemActionErrors(t *testing.T) { Subject: "subject", Body: "body", Attachments: []string{"/file.txt"}, - }, EventParams{ + }, &EventParams{ sender: username, }) assert.Error(t, err) @@ -973,7 +973,7 @@ func TestFilesystemActionErrors(t *testing.T) { Subject: "subject", Body: "body", Attachments: []string{"/file1.txt"}, - }, EventParams{ + }, &EventParams{ sender: username, }) assert.Error(t, err) @@ -1008,7 +1008,7 @@ func TestFilesystemActionErrors(t *testing.T) { }, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1045,7 +1045,7 @@ func TestFilesystemActionErrors(t *testing.T) { Deletes: []string{"/adir/sub/f.dat"}, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1071,7 +1071,7 @@ func TestFilesystemActionErrors(t *testing.T) { MkDirs: []string{"/adir/sub/sub1"}, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1119,7 +1119,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeUserQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1129,7 +1129,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { assert.Error(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeTransferQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1154,7 +1154,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeFolderQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername, @@ -1242,3 +1242,37 @@ func TestScheduledActions(t *testing.T) { assert.NoError(t, err) stopEventScheduler() } + +func TestEventParamsCopy(t *testing.T) { + params := EventParams{ + Name: "name", + Event: "event", + Status: 1, + errors: []string{"error1"}, + } + 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) +} + +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) +} diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 268dbdc8793dec0475916db917da0c41809f431f..414ba54faaf5729347d29d2c3bdb28b21aaa61c2 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -19,6 +19,7 @@ import ( "bytes" "crypto/rand" "encoding/json" + "errors" "fmt" "io" "math" @@ -3064,8 +3065,8 @@ func TestEventRule(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test1@example.com", "test2@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}}", + Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`, + Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}} {{ErrorString}}", }, }, } @@ -3076,7 +3077,7 @@ func TestEventRule(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, Subject: `Failed "{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}}", + Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}} {{ErrorString}}", }, }, } @@ -3187,6 +3188,7 @@ func TestEventRule(t *testing.T) { uploadScriptPath := filepath.Join(os.TempDir(), "upload.sh") u := getTestUser() + u.DownloadDataTransfer = 1 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) movedFileName := "moved.dat" @@ -3230,6 +3232,8 @@ func TestEventRule(t *testing.T) { dirName := "subdir" err = client.Mkdir(dirName) assert.NoError(t, err) + err = client.Mkdir("subdir1") + assert.NoError(t, err) // rule conditions match lastReceivedEmail.reset() err = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client) @@ -3247,7 +3251,32 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username)) + // test the failure action, we download a file that exceeds the transfer quota limit + err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client) + assert.NoError(t, err) + lastReceivedEmail.reset() + f, err := client.Open(path.Join("subdir1", testFileName)) + assert.NoError(t, err) + _, err = io.ReadAll(f) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error()) + } + err = f.Close() + assert.Error(t, err) + assert.Eventually(t, func() bool { + return lastReceivedEmail.get().From != "" + }, 3000*time.Millisecond, 100*time.Millisecond) + email = lastReceivedEmail.get() + assert.Len(t, email.To, 2) + assert.True(t, util.Contains(email.To, "test1@example.com")) + assert.True(t, util.Contains(email.To, "test2@example.com")) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username)) + assert.Contains(t, email.Data, `"download" failed`) + assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error()) + _, err = httpdtest.UpdateTransferQuotaUsage(user, "", http.StatusOK) + assert.NoError(t, err) + // remove the upload script to test the failure action err = os.Remove(uploadScriptPath) assert.NoError(t, err) @@ -3260,10 +3289,11 @@ func TestEventRule(t *testing.T) { email = lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "failure@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name)) // now test the download rule lastReceivedEmail.reset() - f, err := client.Open(movedFileName) + f, err = client.Open(movedFileName) assert.NoError(t, err) contents, err := io.ReadAll(f) assert.NoError(t, err) @@ -3277,7 +3307,7 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username)) } _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) @@ -3291,7 +3321,7 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "delete" from "admin"`) + assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`) _, err = httpdtest.RemoveEventRule(rule3, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) @@ -3443,7 +3473,7 @@ func TestEventRuleProviderEvents(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test3@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "update" from "admin"`) + assert.Contains(t, email.Data, `Subject: New "update" from "admin"`) } // now delete the script to generate an error lastReceivedEmail.reset() @@ -3458,8 +3488,8 @@ func TestEventRuleProviderEvents(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "failure@example.com")) - assert.Contains(t, string(email.Data), `Subject: Failed "update" from "admin"`) - assert.Contains(t, string(email.Data), fmt.Sprintf("Object name: %s object type: folder", folder.Name)) + assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`) + assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name)) lastReceivedEmail.reset() // generate an error for the failure action smtpCfg = smtp.Config{} @@ -3830,8 +3860,8 @@ func TestEventActionEmailAttachments(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username)) - assert.Contains(t, string(email.Data), "Content-Disposition: attachment") + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, "Content-Disposition: attachment") } } @@ -3929,7 +3959,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username)) lastReceivedEmail.reset() // a new upload will not produce a new notification err = writeSFTPFileNoCheck(testFileName+"_1", 32768, client) @@ -3952,7 +3982,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) { email = lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username)) // download again lastReceivedEmail.reset() f, err = client.Open(testFileName) @@ -4001,8 +4031,8 @@ func TestEventRuleCertificate(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}"`, - Body: "Domain: {{Name}} Timestamp: {{Timestamp}}", + Subject: `"{{Event}} {{StatusString}}"`, + Body: "Domain: {{Name}} Timestamp: {{Timestamp}} {{ErrorString}}", }, }, } @@ -4051,11 +4081,13 @@ func TestEventRuleCertificate(t *testing.T) { rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err) + renewalEvent := "Certificate renewal" + common.HandleCertificateEvent(common.EventParams{ Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 1, - Event: "Successful certificate renewal", + Event: renewalEvent, }) assert.Eventually(t, func() bool { return lastReceivedEmail.get().From != "" @@ -4063,24 +4095,28 @@ func TestEventRuleCertificate(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), `Subject: "Successful certificate renewal"`) - assert.Contains(t, string(email.Data), `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent)) + assert.Contains(t, email.Data, `Domain: example.com Timestamp`) lastReceivedEmail.reset() - common.HandleCertificateEvent(common.EventParams{ + params := common.EventParams{ Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 2, - Event: "Certificate renewal failed", - }) + Event: renewalEvent, + } + errRenew := errors.New("generic renew error") + params.AddError(errRenew) + common.HandleCertificateEvent(params) 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, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), `Subject: "Certificate renewal failed"`) - assert.Contains(t, string(email.Data), `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent)) + assert.Contains(t, email.Data, `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, errRenew.Error()) _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) assert.NoError(t, err) @@ -4095,7 +4131,7 @@ func TestEventRuleCertificate(t *testing.T) { Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 1, - Event: "Successful certificate renewal", + Event: renewalEvent, }) smtpCfg = smtp.Config{} @@ -4184,7 +4220,7 @@ func TestEventRuleIPBlocked(t *testing.T) { assert.NoError(t, err) lastReceivedEmail.reset() time.Sleep(300 * time.Millisecond) - assert.Empty(t, lastReceivedEmail.get().From, string(lastReceivedEmail.get().Data)) + assert.Empty(t, lastReceivedEmail.get().From, lastReceivedEmail.get().Data) for i := 0; i < 3; i++ { user.Password = "wrong_pwd" @@ -4203,7 +4239,7 @@ func TestEventRuleIPBlocked(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test3@example.com")) assert.True(t, util.Contains(email.To, "test4@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "IP Blocked"`) + assert.Contains(t, email.Data, `Subject: New "IP Blocked"`) err = dataprovider.DeleteEventRule(rule1.Name, "", "") assert.NoError(t, err) @@ -5305,7 +5341,7 @@ type receivedEmail struct { sync.RWMutex From string To []string - Data []byte + Data string } func (e *receivedEmail) set(from string, to []string, data []byte) { @@ -5314,7 +5350,7 @@ func (e *receivedEmail) set(from string, to []string, data []byte) { e.From = from e.To = to - e.Data = data + e.Data = strings.ReplaceAll(string(data), "=\r\n", "") } func (e *receivedEmail) reset() { @@ -5323,7 +5359,7 @@ func (e *receivedEmail) reset() { e.From = "" e.To = nil - e.Data = nil + e.Data = "" } func (e *receivedEmail) get() receivedEmail { diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index a92c796be4758784f5d708b07a91aa61214e3945..d2f99dde71c2624a7b64ad2cb245031ba678d8b6 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -541,6 +541,12 @@ along with this program. If not, see .

{{`{{Status}}`}} => Status for "upload", "download" and "ssh_cmd" events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.

+

+ {{`{{StatusString}}`}} => Status as string. Possible values "OK", "KO". +

+

+ {{`{{ErrorString}}`}} => Error details. Replaced with an empty string if no errors occur. +

{{`{{VirtualPath}}`}} => Path seen by SFTPGo users, for example "/adir/afile.txt".