From 0da8adb7ac521c2dc2315514f7c84b335f2144c9 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 13 Apr 2025 20:01:03 +0200 Subject: [PATCH] EventManager: breaking change for placeholder names Placeholder names must now be in the format: {{.VirtualPath}} instead of: {{.VirtualPath}} Signed-off-by: Nicola Murino --- internal/common/eventmanager.go | 78 +++++------ internal/common/eventmanager_test.go | 34 ++--- internal/common/protocol_test.go | 138 +++++++++---------- internal/dataprovider/bolt.go | 20 ++- internal/dataprovider/dataprovider.go | 184 +++++++++++++++++++++++++- internal/dataprovider/mysql.go | 20 ++- internal/dataprovider/pgsql.go | 20 ++- internal/dataprovider/sqlcommon.go | 2 +- internal/dataprovider/sqlite.go | 20 ++- internal/httpd/httpd_test.go | 165 +++++++++++++++++++++-- internal/httpd/oidc_test.go | 8 +- templates/webadmin/eventaction.html | 74 +++++------ 12 files changed, 576 insertions(+), 187 deletions(-) diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index 3ae42a15..17e0cd03 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -56,8 +56,8 @@ import ( const ( ipBlockedEventName = "IP Blocked" maxAttachmentsSize = int64(10 * 1024 * 1024) - objDataPlaceholder = "{{ObjectData}}" - objDataPlaceholderString = "{{ObjectDataString}}" + objDataPlaceholder = "{{.ObjectData}}" + objDataPlaceholderString = "{{.ObjectDataString}}" dateTimeMillisFormat = "2006-01-02T15:04:05.000" ) @@ -796,45 +796,45 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s minute := dateTimeString[14:16] replacements := []string{ - "{{Name}}", p.getStringReplacement(p.Name, jsonEscaped), - "{{Event}}", p.Event, - "{{Status}}", fmt.Sprintf("%d", p.Status), - "{{VirtualPath}}", p.getStringReplacement(p.VirtualPath, jsonEscaped), - "{{EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), jsonEscaped), - "{{FsPath}}", p.getStringReplacement(p.FsPath, jsonEscaped), - "{{VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, jsonEscaped), - "{{FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, jsonEscaped), - "{{ObjectName}}", p.getStringReplacement(p.ObjectName, jsonEscaped), - "{{ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), jsonEscaped), - "{{ObjectType}}", p.ObjectType, - "{{FileSize}}", strconv.FormatInt(p.FileSize, 10), - "{{Elapsed}}", strconv.FormatInt(p.Elapsed, 10), - "{{Protocol}}", p.Protocol, - "{{IP}}", p.IP, - "{{Role}}", p.getStringReplacement(p.Role, jsonEscaped), - "{{Email}}", p.getStringReplacement(p.Email, jsonEscaped), - "{{Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10), - "{{DateTime}}", dateTimeString, - "{{Year}}", year, - "{{Month}}", month, - "{{Day}}", day, - "{{Hour}}", hour, - "{{Minute}}", minute, - "{{StatusString}}", p.getStatusString(), - "{{UID}}", p.getStringReplacement(p.UID, jsonEscaped), - "{{Ext}}", p.getStringReplacement(p.Extension, jsonEscaped), + "{{.Name}}", p.getStringReplacement(p.Name, jsonEscaped), + "{{.Event}}", p.Event, + "{{.Status}}", fmt.Sprintf("%d", p.Status), + "{{.VirtualPath}}", p.getStringReplacement(p.VirtualPath, jsonEscaped), + "{{.EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), jsonEscaped), + "{{.FsPath}}", p.getStringReplacement(p.FsPath, jsonEscaped), + "{{.VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, jsonEscaped), + "{{.FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, jsonEscaped), + "{{.ObjectName}}", p.getStringReplacement(p.ObjectName, jsonEscaped), + "{{.ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), jsonEscaped), + "{{.ObjectType}}", p.ObjectType, + "{{.FileSize}}", strconv.FormatInt(p.FileSize, 10), + "{{.Elapsed}}", strconv.FormatInt(p.Elapsed, 10), + "{{.Protocol}}", p.Protocol, + "{{.IP}}", p.IP, + "{{.Role}}", p.getStringReplacement(p.Role, jsonEscaped), + "{{.Email}}", p.getStringReplacement(p.Email, jsonEscaped), + "{{.Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10), + "{{.DateTime}}", dateTimeString, + "{{.Year}}", year, + "{{.Month}}", month, + "{{.Day}}", day, + "{{.Hour}}", hour, + "{{.Minute}}", minute, + "{{.StatusString}}", p.getStatusString(), + "{{.UID}}", p.getStringReplacement(p.UID, jsonEscaped), + "{{.Ext}}", p.getStringReplacement(p.Extension, jsonEscaped), } if p.VirtualPath != "" { - replacements = append(replacements, "{{VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), jsonEscaped)) + replacements = append(replacements, "{{.VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), jsonEscaped)) } if p.VirtualTargetPath != "" { - replacements = append(replacements, "{{VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), jsonEscaped)) - replacements = append(replacements, "{{TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), jsonEscaped)) + replacements = append(replacements, "{{.VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), jsonEscaped)) + replacements = append(replacements, "{{.TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), jsonEscaped)) } if len(p.errors) > 0 { - replacements = append(replacements, "{{ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), jsonEscaped)) + replacements = append(replacements, "{{.ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), jsonEscaped)) } else { - replacements = append(replacements, "{{ErrorString}}", "") + replacements = append(replacements, "{{.ErrorString}}", "") } replacements = append(replacements, objDataPlaceholder, "{}") replacements = append(replacements, objDataPlaceholderString, "") @@ -848,11 +848,11 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s } if p.IDPCustomFields != nil { for k, v := range *p.IDPCustomFields { - replacements = append(replacements, fmt.Sprintf("{{IDPField%s}}", k), p.getStringReplacement(v, jsonEscaped)) + replacements = append(replacements, fmt.Sprintf("{{.IDPField%s}}", k), p.getStringReplacement(v, jsonEscaped)) } } - replacements = append(replacements, "{{Metadata}}", "{}") - replacements = append(replacements, "{{MetadataString}}", "") + replacements = append(replacements, "{{.Metadata}}", "{}") + replacements = append(replacements, "{{.MetadataString}}", "") if len(p.Metadata) > 0 { data, err := json.Marshal(p.Metadata) if err == nil { @@ -1193,7 +1193,7 @@ func getMailAttachments(conn *BaseConnection, attachments []string, replacer *st } func replaceWithReplacer(input string, replacer *strings.Replacer) string { - if !strings.Contains(input, "{{") { + if !strings.Contains(input, "{{.") { return input } return replacer.Replace(input) @@ -1280,7 +1280,7 @@ func getHTTPRuleActionEndpoint(c *dataprovider.EventActionHTTPConfig, replacer * if err != nil { return "", fmt.Errorf("invalid endpoint: %w", err) } - if strings.Contains(u.Path, "{{") { + if strings.Contains(u.Path, "{{.") { pathComponents := strings.Split(u.Path, "/") for idx := range pathComponents { part := replaceWithReplacer(pathComponents[idx], replacer) diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index ff8a6e64..d3d915d6 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -810,17 +810,17 @@ func TestDateTimePlaceholder(t *testing.T) { } replacements := params.getStringReplacements(false, false) r := strings.NewReplacer(replacements...) - res := r.Replace("{{DateTime}}") + res := r.Replace("{{.DateTime}}") assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res) - res = r.Replace("{{Year}}-{{Month}}-{{Day}}T{{Hour}}:{{Minute}}") + res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}") assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat)[:16], res) Config.TZ = "local" replacements = params.getStringReplacements(false, false) r = strings.NewReplacer(replacements...) - res = r.Replace("{{DateTime}}") + res = r.Replace("{{.DateTime}}") assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res) - res = r.Replace("{{Year}}-{{Month}}-{{Day}}T{{Hour}}:{{Minute}}") + res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}") assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat)[:16], res) Config.TZ = oldTZ @@ -845,7 +845,7 @@ func TestEventRuleActions(t *testing.T) { HTTPConfig: dataprovider.EventActionHTTPConfig{ Endpoint: "http://foo\x7f.com/", // invalid URL SkipTLSVerify: true, - Body: `"data": "{{ObjectDataString}}"`, + Body: `"data": "{{.ObjectDataString}}"`, Method: http.MethodPost, QueryParameters: []dataprovider.KeyValue{ { @@ -913,7 +913,7 @@ func TestEventRuleActions(t *testing.T) { assert.Contains(t, getErrorString(err), "error getting user") action.Options.HTTPConfig.Parts = nil - action.Options.HTTPConfig.Body = "{{ObjectData}}" + action.Options.HTTPConfig.Body = "{{.ObjectData}}" // test disk and transfer quota reset username1 := "user1" username2 := "user2" @@ -1249,7 +1249,7 @@ func TestEventRuleActions(t *testing.T) { Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ Name: "test.zip", - Paths: []string{"/{{VirtualPath}}"}, + Paths: []string{"/{{.VirtualPath}}"}, }, }, } @@ -1970,7 +1970,7 @@ func TestScheduledActions(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionMkdirs, - MkDirs: []string{"{{Year}}_{{Month}}_{{Day}}"}, + MkDirs: []string{"{{.Year}}_{{.Month}}_{{.Day}}"}, }, }, } @@ -2159,11 +2159,11 @@ func TestWriteHTTPPartsError(t *testing.T) { } func TestReplacePathsPlaceholders(t *testing.T) { - replacer := strings.NewReplacer("{{VirtualPath}}", "/path1") - paths := []string{"{{VirtualPath}}", "/path1"} + replacer := strings.NewReplacer("{{.VirtualPath}}", "/path1") + paths := []string{"{{.VirtualPath}}", "/path1"} paths = replacePathsPlaceholders(paths, replacer) assert.Equal(t, []string{"/path1"}, paths) - paths = []string{"{{VirtualPath}}", "/path2"} + paths = []string{"{{.VirtualPath}}", "/path2"} paths = replacePathsPlaceholders(paths, replacer) assert.Equal(t, []string{"/path1", "/path2"}, paths) } @@ -2256,7 +2256,7 @@ func TestOnDemandRule(t *testing.T) { Recipients: []string{"example@example.org"}, Subject: "subject", Body: "body", - Attachments: []string{"/{{VirtualPath}}"}, + Attachments: []string{"/{{.VirtualPath}}"}, }, }, } @@ -2297,21 +2297,21 @@ func getErrorString(err error) string { func TestHTTPEndpointWithPlaceholders(t *testing.T) { c := dataprovider.EventActionHTTPConfig{ - Endpoint: "http://127.0.0.1:8080/base/url/{{Name}}/{{VirtualPath}}/upload", + Endpoint: "http://127.0.0.1:8080/base/url/{{.Name}}/{{.VirtualPath}}/upload", QueryParameters: []dataprovider.KeyValue{ { Key: "u", - Value: "{{Name}}", + Value: "{{.Name}}", }, { Key: "p", - Value: "{{VirtualPath}}", + Value: "{{.VirtualPath}}", }, }, } name := "uname" vPath := "/a dir/@ file.txt" - replacer := strings.NewReplacer("{{Name}}", name, "{{VirtualPath}}", vPath) + 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) + @@ -2333,7 +2333,7 @@ func TestMetadataReplacement(t *testing.T) { } replacements := params.getStringReplacements(false, false) replacer := strings.NewReplacer(replacements...) - reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{Metadata}} {{MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false) + reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{.Metadata}} {{.MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false) require.NoError(t, err) data, err := io.ReadAll(reader) require.NoError(t, err) diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index b79dc592..9f57e272 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -3784,8 +3784,8 @@ func TestEventRule(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test1@example.com", "test2@example.com"}, Bcc: []string{"test3@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}} {{ErrorString}}", + Subject: `New "{{.Event}}" from "{{.Name}}" status {{.StatusString}}`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} Data: {{.ObjectData}} {{.ErrorString}}", }, }, } @@ -3795,8 +3795,8 @@ func TestEventRule(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, - Subject: `Failed "{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}} {{ErrorString}}", + Subject: `Failed "{{.Event}}" from "{{.Name}}"`, + Body: "Fs path {{.FsPath}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.ErrorString}}", }, }, } @@ -3941,7 +3941,7 @@ func TestEventRule(t *testing.T) { EnvVars: []dataprovider.KeyValue{ { Key: "SFTPGO_ACTION_PATH", - Value: "{{FsPath}}", + Value: "{{.FsPath}}", }, { Key: "CUSTOM_ENV_VAR", @@ -4050,7 +4050,7 @@ func TestEventRule(t *testing.T) { assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username)) } // test upload action command with arguments - action1.Options.CmdConfig.Args = []string{"{{Event}}", "{{VirtualPath}}", "custom_arg"} + action1.Options.CmdConfig.Args = []string{"{{.Event}}", "{{.VirtualPath}}", "custom_arg"} action1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK) assert.NoError(t, err) uploadLogFilePath := filepath.Join(os.TempDir(), "upload.log") @@ -4128,8 +4128,8 @@ func TestEventRuleStatues(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test6@example.com"}, - Subject: `New "{{Event}}" error`, - Body: "{{ErrorString}}", + Subject: `New "{{.Event}}" error`, + Body: "{{.ErrorString}}", }, }, } @@ -4241,7 +4241,7 @@ func TestEventRuleDisabledCommand(t *testing.T) { EnvVars: []dataprovider.KeyValue{ { Key: "SFTPGO_OBJECT_DATA", - Value: "{{ObjectData}}", + Value: "{{.ObjectData}}", }, }, }, @@ -4253,8 +4253,8 @@ func TestEventRuleDisabledCommand(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test3@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}"`, - Body: "Object name: {{ObjectName}} object type: {{ObjectType}} Data: {{ObjectData}}", + Subject: `New "{{.Event}}" from "{{.Name}}"`, + Body: "Object name: {{.ObjectName}} object type: {{.ObjectType}} Data: {{.ObjectData}}", }, }, } @@ -4265,8 +4265,8 @@ func TestEventRuleDisabledCommand(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, - Subject: `Failed "{{Event}}" from "{{Name}}"`, - Body: "Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}", + Subject: `Failed "{{.Event}}" from "{{.Name}}"`, + Body: "Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}", }, }, } @@ -4390,7 +4390,7 @@ func TestEventRuleProviderEvents(t *testing.T) { EnvVars: []dataprovider.KeyValue{ { Key: "SFTPGO_OBJECT_DATA", - Value: "{{ObjectData}}", + Value: "{{.ObjectData}}", }, }, }, @@ -4402,8 +4402,8 @@ func TestEventRuleProviderEvents(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test3@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}"`, - Body: "Object name: {{ObjectName}} object type: {{ObjectType}} Data: {{ObjectData}}", + Subject: `New "{{.Event}}" from "{{.Name}}"`, + Body: "Object name: {{.ObjectName}} object type: {{.ObjectType}} Data: {{.ObjectData}}", }, }, } @@ -4414,8 +4414,8 @@ func TestEventRuleProviderEvents(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, - Subject: `Failed "{{Event}}" from "{{Name}}"`, - Body: "Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}", + Subject: `Failed "{{.Event}}" from "{{.Name}}"`, + Body: "Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}", }, }, } @@ -4561,8 +4561,8 @@ func TestEventRuleFsActions(t *testing.T) { Renames: []dataprovider.RenameConfig{ { KeyValue: dataprovider.KeyValue{ - Key: "/{{VirtualDirPath}}/{{ObjectName}}", - Value: "/{{ObjectName}}_renamed", + Key: "/{{.VirtualDirPath}}/{{.ObjectName}}", + Value: "/{{.ObjectName}}_renamed", }, }, }, @@ -4575,7 +4575,7 @@ func TestEventRuleFsActions(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionDelete, - Deletes: []string{"/{{ObjectName}}_renamed"}, + Deletes: []string{"/{{.ObjectName}}_renamed"}, }, }, } @@ -4593,7 +4593,7 @@ func TestEventRuleFsActions(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionExist, - Exist: []string{"/{{VirtualPath}}"}, + Exist: []string{"/{{.VirtualPath}}"}, }, }, } @@ -4831,8 +4831,8 @@ func TestEventActionObjectBaseName(t *testing.T) { Renames: []dataprovider.RenameConfig{ { KeyValue: dataprovider.KeyValue{ - Key: "/{{VirtualDirPath}}/{{ObjectName}}", - Value: "/{{ObjectBaseName}}", + Key: "/{{.VirtualDirPath}}/{{.ObjectName}}", + Value: "/{{.ObjectBaseName}}", }, }, }, @@ -4911,8 +4911,8 @@ func TestUploadEventRule(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test1@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}} {{ErrorString}}", + Subject: `New "{{.Event}}" from "{{.Name}}" status {{.StatusString}}`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} Data: {{.ObjectData}} {{.ErrorString}}", }, }, } @@ -5047,8 +5047,8 @@ func TestEventRulePreDelete(t *testing.T) { Renames: []dataprovider.RenameConfig{ { KeyValue: dataprovider.KeyValue{ - Key: "/{{VirtualPath}}", - Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath), + Key: "/{{.VirtualPath}}", + Value: fmt.Sprintf("/%s/{{.VirtualPath}}", movePath), }, UpdateModTime: true, }, @@ -5410,7 +5410,7 @@ func TestFsActionCopy(t *testing.T) { Type: dataprovider.FilesystemActionCopy, Copy: []dataprovider.KeyValue{ { - Key: "/{{VirtualPath}}/", + Key: "/{{.VirtualPath}}/", Value: "/dircopy/", }, }, @@ -5492,8 +5492,8 @@ func TestEventFsActionsGroupFilters(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"example@example.net"}, - Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} {{ErrorString}}", + Subject: `New "{{.Event}}" from "{{.Name}}" status {{.StatusString}}`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.ErrorString}}", }, }, } @@ -5623,8 +5623,8 @@ func TestEventProviderActionGroupFilters(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"example@example.net"}, - Subject: `New "{{Event}}" from "{{Name}}"`, - Body: "IP: {{IP}}", + Subject: `New "{{.Event}}" from "{{.Name}}"`, + Body: "IP: {{.IP}}", }, }, } @@ -5759,9 +5759,9 @@ func TestBackupAsAttachment(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}} {{StatusString}}"`, - Body: "Domain: {{Name}}", - Attachments: []string{"/{{VirtualPath}}"}, + Subject: `"{{.Event}} {{.StatusString}}"`, + Body: "Domain: {{.Name}}", + Attachments: []string{"/{{.VirtualPath}}"}, }, }, } @@ -5840,11 +5840,11 @@ func TestEventActionHTTPMultipart(t *testing.T) { Value: "application/json", }, }, - Body: `{"FilePath": "{{VirtualPath}}"}`, + Body: `{"FilePath": "{{.VirtualPath}}"}`, }, { Name: "file", - Filepath: "/{{VirtualPath}}", + Filepath: "/{{.VirtualPath}}", }, }, }, @@ -5920,8 +5920,8 @@ func TestEventActionCompress(t *testing.T) { FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ - Name: "/{{VirtualPath}}.zip", - Paths: []string{"/{{VirtualPath}}"}, + Name: "/{{.VirtualPath}}.zip", + Paths: []string{"/{{.VirtualPath}}"}, }, }, }, @@ -6091,7 +6091,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, Subject: `"Compress failed"`, - Body: "Error: {{ErrorString}}", + Body: "Error: {{.ErrorString}}", }, }, } @@ -6235,8 +6235,8 @@ func TestEventActionCompressQuotaFolder(t *testing.T) { FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ - Name: "/{{VirtualPath}}.zip", - Paths: []string{"/{{VirtualPath}}", testDir}, + Name: "/{{.VirtualPath}}.zip", + Paths: []string{"/{{.VirtualPath}}", testDir}, }, }, }, @@ -6361,8 +6361,8 @@ func TestEventActionCompressErrors(t *testing.T) { FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ - Name: "/{{VirtualPath}}.zip", - Paths: []string{"/{{VirtualPath}}.zip"}, // cannot compress itself + Name: "/{{.VirtualPath}}.zip", + Paths: []string{"/{{.VirtualPath}}.zip"}, // cannot compress itself }, }, }, @@ -6424,7 +6424,7 @@ func TestEventActionCompressErrors(t *testing.T) { // try to overwrite a directory testDir := "/adir" action1.Options.FsConfig.Compress.Name = testDir - action1.Options.FsConfig.Compress.Paths = []string{"/{{VirtualPath}}"} + action1.Options.FsConfig.Compress.Paths = []string{"/{{.VirtualPath}}"} _, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK) assert.NoError(t, err) conn, client, err = getSftpClient(user) @@ -6469,8 +6469,8 @@ func TestEventActionEmailAttachments(t *testing.T) { FsConfig: dataprovider.EventActionFilesystemConfig{ Type: dataprovider.FilesystemActionCompress, Compress: dataprovider.EventActionFsCompress{ - Name: "/archive/{{VirtualPath}}.zip", - Paths: []string{"/{{VirtualPath}}"}, + Name: "/archive/{{.VirtualPath}}.zip", + Paths: []string{"/{{.VirtualPath}}"}, }, }, }, @@ -6483,9 +6483,9 @@ func TestEventActionEmailAttachments(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} {{EscapedVirtualPath}}", - Attachments: []string{"/archive/{{VirtualPath}}.zip"}, + Subject: `"{{.Event}}" from "{{.Name}}"`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.EscapedVirtualPath}}", + Attachments: []string{"/archive/{{.VirtualPath}}.zip"}, }, }, } @@ -6604,8 +6604,8 @@ func TestEventActionsRetentionReports(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}}", + Subject: `"{{.Event}}" from "{{.Name}}"`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}}", Attachments: []string{dataprovider.RetentionReportPlaceHolder}, }, }, @@ -6832,8 +6832,8 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}}", + Subject: `"{{.Event}}" from "{{.Name}}"`, + Body: "Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}}", }, }, } @@ -6964,9 +6964,9 @@ func TestEventRuleRenameEvent(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}" from "{{Name}}"`, + Subject: `"{{.Event}}" from "{{.Name}}"`, ContentType: 1, - Body: `

Fs path {{FsPath}}, Target path "{{VirtualTargetDirPath}}/{{TargetName}}", size: {{FileSize}}

`, + Body: `

Fs path {{.FsPath}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}

`, }, }, } @@ -7045,9 +7045,9 @@ func TestEventRuleIDPLogin(t *testing.T) { username := `test_"idp_"login` custom1 := `cust"oa"1` u := map[string]any{ - "username": "{{Name}}", + "username": "{{.Name}}", "status": 1, - "home_dir": filepath.Join(os.TempDir(), "{{IDPFieldcustom1}}"), + "home_dir": filepath.Join(os.TempDir(), "{{.IDPFieldcustom1}}"), "permissions": map[string][]string{ "/": {dataprovider.PermAny}, }, @@ -7055,7 +7055,7 @@ func TestEventRuleIDPLogin(t *testing.T) { userTmpl, err := json.Marshal(u) require.NoError(t, err) a := map[string]any{ - "username": "{{Name}}", + "username": "{{.Name}}", "status": 1, "permissions": []string{dataprovider.PermAdminAny}, } @@ -7081,8 +7081,8 @@ func TestEventRuleIDPLogin(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}} {{StatusString}}"`, - Body: "{{Name}} Custom field: {{IDPFieldcustom1}}", + Subject: `"{{.Event}} {{.StatusString}}"`, + Body: "{{.Name}} Custom field: {{.IDPFieldcustom1}}", }, }, } @@ -7327,8 +7327,8 @@ func TestEventRuleEmailField(t *testing.T) { Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ - Recipients: []string{"{{Email}}"}, - Subject: `"{{Event}}" from "{{Name}}"`, + Recipients: []string{"{{.Email}}"}, + Subject: `"{{.Event}}" from "{{.Name}}"`, Body: "Sample email body", }, }, @@ -7342,7 +7342,7 @@ func TestEventRuleEmailField(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, Subject: `"Failure`, - Body: "{{ErrorString}}", + Body: "{{.ErrorString}}", }, }, } @@ -7473,9 +7473,9 @@ func TestEventRuleCertificate(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}} {{StatusString}}"`, + Subject: `"{{.Event}} {{.StatusString}}"`, ContentType: 0, - Body: "Domain: {{Name}} Timestamp: {{Timestamp}} {{ErrorString}} Date time: {{DateTime}}", + Body: "Domain: {{.Name}} Timestamp: {{.Timestamp}} {{.ErrorString}} Date time: {{.DateTime}}", }, }, } @@ -7613,8 +7613,8 @@ func TestEventRuleIPBlocked(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test3@example.com", "test4@example.com"}, - Subject: `New "{{Event}}"`, - Body: "IP: {{IP}} Timestamp: {{Timestamp}}", + Subject: `New "{{.Event}}"`, + Body: "IP: {{.IP}} Timestamp: {{.Timestamp}}", }, }, } diff --git a/internal/dataprovider/bolt.go b/internal/dataprovider/bolt.go index 104e9dbb..64fb238f 100644 --- a/internal/dataprovider/bolt.go +++ b/internal/dataprovider/bolt.go @@ -40,7 +40,7 @@ import ( ) const ( - boltDatabaseVersion = 31 + boltDatabaseVersion = 32 ) var ( @@ -3186,10 +3186,13 @@ func (p *BoltProvider) migrateDatabase() error { providerLog(logger.LevelError, "%v", err) logger.ErrorToConsole("%v", err) return err - case version == 29, version == 30: - logger.InfoToConsole("updating database schema version: %d -> 31", version) - providerLog(logger.LevelInfo, "updating database schema version: %d -> 31", version) - return updateBoltDatabaseVersion(p.dbHandle, 31) + case version == 29, version == 30, version == 31: + logger.InfoToConsole("updating database schema version: %d -> 32", version) + providerLog(logger.LevelInfo, "updating database schema version: %d -> 32", version) + if err := updateEventActions(); err != nil { + return err + } + return updateBoltDatabaseVersion(p.dbHandle, 32) default: if version > boltDatabaseVersion { providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, @@ -3211,9 +3214,14 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocycl return errors.New("current version match target version, nothing to do") } switch dbVersion.Version { - case 30, 31: + case 30, 31, 32: logger.InfoToConsole("downgrading database schema version: %d -> 29", dbVersion.Version) providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 29", dbVersion.Version) + if dbVersion.Version == 32 { + if err := restoreEventActions(); err != nil { + return err + } + } return updateBoltDatabaseVersion(p.dbHandle, 29) default: return fmt.Errorf("database schema version not handled: %v", dbVersion.Version) diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 9d523ca7..53a96294 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -27,6 +27,7 @@ import ( "crypto/sha512" "crypto/subtle" "crypto/x509" + "database/sql" "encoding/base64" "encoding/hex" "encoding/json" @@ -90,7 +91,7 @@ const ( CockroachDataProviderName = "cockroachdb" // DumpVersion defines the version for the dump. // For restore/load we support the current version and the previous one - DumpVersion = 16 + DumpVersion = 17 argonPwdPrefix = "$argon2id$" bcryptPwdPrefix = "$2a$" @@ -2557,6 +2558,17 @@ func DumpData(scopes []string) (BackupData, error) { func ParseDumpData(data []byte) (BackupData, error) { var dump BackupData err := json.Unmarshal(data, &dump) + if err != nil { + return dump, err + } + if dump.Version < 17 { + providerLog(logger.LevelInfo, "updating placeholders for actions restored from dump version %d", dump.Version) + eventActions, err := updateEventActionPlaceholders(dump.EventActions) + if err != nil { + return dump, fmt.Errorf("unable to update event action placeholders for dump version %d: %w", dump.Version, err) + } + dump.EventActions = eventActions + } return dump, err } @@ -4673,6 +4685,176 @@ func isExternalAuthConfigured(loginMethod string) bool { } } +func replaceTemplateVars(input string) string { + var result strings.Builder + i := 0 + for i < len(input) { + if i+2 <= len(input) && input[i:i+2] == "{{" { + if i+2 < len(input) { + nextChar := input[i+2] + if nextChar == ' ' || nextChar == '.' || nextChar == '-' { + // Don't replace if followed by space, dot or minus. + result.WriteString("{{") + i += 2 + continue + } + } + + // Find the closing "}}" + closing := strings.Index(input[i:], "}}") + if closing != -1 { + // Replace with {{. only if it's a proper template variable. + result.WriteString("{{.") + result.WriteString(input[i+2 : i+closing]) + result.WriteString("}}") + i += closing + 2 + continue + } + } + result.WriteByte(input[i]) + i++ + } + return result.String() +} + +func restoreTemplateVars(input string) string { + var result strings.Builder + i := 0 + + for i < len(input) { + if i+3 <= len(input) && input[i:i+3] == "{{." { + if i+3 < len(input) { + nextChar := input[i+3] + if nextChar == ' ' || nextChar == '.' || nextChar == '-' { + // Don't change if it's a space, dot, or minus + result.WriteString("{{.") + i += 3 + continue + } + } + // Find the closing "}}" + closing := strings.Index(input[i:], "}}") + if closing != -1 { + // Strip the dot and write the rest + result.WriteString("{{") + result.WriteString(input[i+3 : i+closing]) + result.WriteString("}}") + i += closing + 2 + continue + } + } + + result.WriteByte(input[i]) + i++ + } + + return result.String() +} + +func updateEventActionPlaceholders(actions []BaseEventAction) ([]BaseEventAction, error) { + var result []BaseEventAction + + for _, action := range actions { + options, err := json.Marshal(action.Options) + if err != nil { + return nil, err + } + convertedOptions := replaceTemplateVars(string(options)) + var opts BaseEventActionOptions + err = json.Unmarshal([]byte(convertedOptions), &opts) + if err != nil { + return nil, err + } + action.Options = opts + result = append(result, action) + } + + return result, nil +} + +func restoreEventActionsPlaceholders(actions []BaseEventAction) ([]BaseEventAction, error) { + var result []BaseEventAction + + for _, action := range actions { + options, err := json.Marshal(action.Options) + if err != nil { + return nil, err + } + convertedOptions := restoreTemplateVars(string(options)) + var opts BaseEventActionOptions + err = json.Unmarshal([]byte(convertedOptions), &opts) + if err != nil { + return nil, err + } + action.Options = opts + result = append(result, action) + } + + return result, nil +} + +func updateEventActions() error { + actions, err := provider.dumpEventActions() + if err != nil { + return err + } + convertedActions, err := updateEventActionPlaceholders(actions) + if err != nil { + return err + } + for _, action := range convertedActions { + providerLog(logger.LevelInfo, "updating placeholders for event action %q", action.Name) + if err := provider.updateEventAction(&action); err != nil { + return fmt.Errorf("unable to save updated event action %q: %w", action.Name, err) + } + } + return nil +} + +func restoreEventActions() error { + actions, err := provider.dumpEventActions() + if err != nil { + return err + } + convertedActions, err := restoreEventActionsPlaceholders(actions) + if err != nil { + return err + } + for _, action := range convertedActions { + providerLog(logger.LevelInfo, "restoring placeholders for event action %q", action.Name) + if err := provider.updateEventAction(&action); err != nil { + return fmt.Errorf("unable to save updated event action %q: %w", action.Name, err) + } + } + return nil +} + +func updateSQLDatabaseFrom31To32(dbHandle *sql.DB) error { + logger.InfoToConsole("updating database data version: 31 -> 32") + providerLog(logger.LevelInfo, "updating database data version: 31 -> 32") + + if err := updateEventActions(); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) + defer cancel() + + return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 32) +} + +func downgradeSQLDatabaseFrom32To31(dbHandle *sql.DB) error { + logger.InfoToConsole("downgrading database data version: 32 -> 31") + providerLog(logger.LevelInfo, "downgrading database data version: 32 -> 31") + + if err := restoreEventActions(); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) + defer cancel() + + return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 31) +} + func getConfigPath(name, configDir string) string { if !util.IsFileInputValid(name) { return "" diff --git a/internal/dataprovider/mysql.go b/internal/dataprovider/mysql.go index b35a6691..9bd1d8d9 100644 --- a/internal/dataprovider/mysql.go +++ b/internal/dataprovider/mysql.go @@ -818,6 +818,8 @@ func (p *MySQLProvider) migrateDatabase() error { return updateMySQLDatabaseFromV29(p.dbHandle) case version == 30: return updateMySQLDatabaseFromV30(p.dbHandle) + case version == 31: + return updateMySQLDatabaseFromV31(p.dbHandle) default: if version > sqlDatabaseVersion { providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, @@ -844,6 +846,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error { return downgradeMySQLDatabaseFromV30(p.dbHandle) case 31: return downgradeMySQLDatabaseFromV31(p.dbHandle) + case 32: + return downgradeMySQLDatabaseFromV32(p.dbHandle) default: return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) } @@ -890,7 +894,14 @@ func updateMySQLDatabaseFromV29(dbHandle *sql.DB) error { } func updateMySQLDatabaseFromV30(dbHandle *sql.DB) error { - return updateMySQLDatabaseFrom30To31(dbHandle) + if err := updateMySQLDatabaseFrom30To31(dbHandle); err != nil { + return err + } + return updateMySQLDatabaseFromV31(dbHandle) +} + +func updateMySQLDatabaseFromV31(dbHandle *sql.DB) error { + return updateSQLDatabaseFrom31To32(dbHandle) } func downgradeMySQLDatabaseFromV30(dbHandle *sql.DB) error { @@ -904,6 +915,13 @@ func downgradeMySQLDatabaseFromV31(dbHandle *sql.DB) error { return downgradeMySQLDatabaseFromV30(dbHandle) } +func downgradeMySQLDatabaseFromV32(dbHandle *sql.DB) error { + if err := downgradeSQLDatabaseFrom32To31(dbHandle); err != nil { + return err + } + return downgradeMySQLDatabaseFromV31(dbHandle) +} + func updateMySQLDatabaseFrom29To30(dbHandle *sql.DB) error { logger.InfoToConsole("updating database schema version: 29 -> 30") providerLog(logger.LevelInfo, "updating database schema version: 29 -> 30") diff --git a/internal/dataprovider/pgsql.go b/internal/dataprovider/pgsql.go index 19f4c35b..51bdc2f1 100644 --- a/internal/dataprovider/pgsql.go +++ b/internal/dataprovider/pgsql.go @@ -843,6 +843,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl return updatePGSQLDatabaseFromV29(p.dbHandle) case version == 30: return updatePGSQLDatabaseFromV30(p.dbHandle) + case version == 31: + return updatePGSQLDatabaseFromV31(p.dbHandle) default: if version > sqlDatabaseVersion { providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, @@ -869,6 +871,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error { return downgradePGSQLDatabaseFromV30(p.dbHandle) case 31: return downgradePGSQLDatabaseFromV31(p.dbHandle) + case 32: + return downgradePGSQLDatabaseFromV32(p.dbHandle) default: return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) } @@ -915,7 +919,14 @@ func updatePGSQLDatabaseFromV29(dbHandle *sql.DB) error { } func updatePGSQLDatabaseFromV30(dbHandle *sql.DB) error { - return updatePGSQLDatabaseFrom30To31(dbHandle) + if err := updatePGSQLDatabaseFrom30To31(dbHandle); err != nil { + return err + } + return updatePGSQLDatabaseFromV31(dbHandle) +} + +func updatePGSQLDatabaseFromV31(dbHandle *sql.DB) error { + return updateSQLDatabaseFrom31To32(dbHandle) } func downgradePGSQLDatabaseFromV30(dbHandle *sql.DB) error { @@ -929,6 +940,13 @@ func downgradePGSQLDatabaseFromV31(dbHandle *sql.DB) error { return downgradePGSQLDatabaseFromV30(dbHandle) } +func downgradePGSQLDatabaseFromV32(dbHandle *sql.DB) error { + if err := downgradeSQLDatabaseFrom32To31(dbHandle); err != nil { + return err + } + return downgradePGSQLDatabaseFromV31(dbHandle) +} + func updatePGSQLDatabaseFrom29To30(dbHandle *sql.DB) error { logger.InfoToConsole("updating database schema version: 29 -> 30") providerLog(logger.LevelInfo, "updating database schema version: 29 -> 30") diff --git a/internal/dataprovider/sqlcommon.go b/internal/dataprovider/sqlcommon.go index 183166f1..aba4675f 100644 --- a/internal/dataprovider/sqlcommon.go +++ b/internal/dataprovider/sqlcommon.go @@ -36,7 +36,7 @@ import ( ) const ( - sqlDatabaseVersion = 31 + sqlDatabaseVersion = 32 defaultSQLQueryTimeout = 10 * time.Second longSQLQueryTimeout = 60 * time.Second ) diff --git a/internal/dataprovider/sqlite.go b/internal/dataprovider/sqlite.go index 27ded2bc..af37dd55 100644 --- a/internal/dataprovider/sqlite.go +++ b/internal/dataprovider/sqlite.go @@ -741,6 +741,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl return updateSQLiteDatabaseFromV29(p.dbHandle) case version == 30: return updateSQLiteDatabaseFromV30(p.dbHandle) + case version == 31: + return updateSQLiteDatabaseFromV31(p.dbHandle) default: if version > sqlDatabaseVersion { providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, @@ -767,6 +769,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error { return downgradeSQLiteDatabaseFromV30(p.dbHandle) case 31: return downgradeSQLiteDatabaseFromV31(p.dbHandle) + case 32: + return downgradeSQLiteDatabaseFromV32(p.dbHandle) default: return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) } @@ -820,7 +824,14 @@ func updateSQLiteDatabaseFromV29(dbHandle *sql.DB) error { } func updateSQLiteDatabaseFromV30(dbHandle *sql.DB) error { - return updateSQLiteDatabaseFrom30To31(dbHandle) + if err := updateSQLiteDatabaseFrom30To31(dbHandle); err != nil { + return err + } + return updateSQLiteDatabaseFromV31(dbHandle) +} + +func updateSQLiteDatabaseFromV31(dbHandle *sql.DB) error { + return updateSQLDatabaseFrom31To32(dbHandle) } func downgradeSQLiteDatabaseFromV30(dbHandle *sql.DB) error { @@ -834,6 +845,13 @@ func downgradeSQLiteDatabaseFromV31(dbHandle *sql.DB) error { return downgradeSQLiteDatabaseFromV30(dbHandle) } +func downgradeSQLiteDatabaseFromV32(dbHandle *sql.DB) error { + if err := downgradeSQLDatabaseFrom32To31(dbHandle); err != nil { + return err + } + return downgradeSQLiteDatabaseFromV31(dbHandle) +} + func updateSQLiteDatabaseFrom29To30(dbHandle *sql.DB) error { logger.InfoToConsole("updating database schema version: 29 -> 30") providerLog(logger.LevelInfo, "updating database schema version: 29 -> 30") diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index e8a306c9..79ff65e7 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -628,6 +628,72 @@ func TestInitialization(t *testing.T) { assert.NoError(t, err) } +func TestMigrateEventActionPlaceholders(t *testing.T) { + if config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName { + t.Skip("this test is not supported with the memory provider") + } + // Add some event actions using the old placeholders syntax + a1 := dataprovider.BaseEventAction{ + Name: xid.New().String(), + Type: dataprovider.ActionTypeEmail, + Options: dataprovider.BaseEventActionOptions{ + EmailConfig: dataprovider.EventActionEmailConfig{ + Recipients: []string{"failure@example.com"}, + Subject: `Failed "{{Event}}" from "{{Name}}"`, + Body: "Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}", + }, + }, + } + a2 := dataprovider.BaseEventAction{ + Name: xid.New().String(), + Type: dataprovider.ActionTypeFilesystem, + Options: dataprovider.BaseEventActionOptions{ + FsConfig: dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionRename, + Renames: []dataprovider.RenameConfig{ + { + KeyValue: dataprovider.KeyValue{ + Key: "/{{VirtualDirPath}}/{{ObjectName}}", + Value: "/{{ObjectName}}_renamed", + }, + }, + }, + }, + }, + } + action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) + assert.NoError(t, err) + action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) + assert.NoError(t, err) + // Revert the database to the previous version. + err = dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + err = dataprovider.RevertDatabase(providerConf, configDir, 29) + assert.NoError(t, err) + // Close and initialize. + err = dataprovider.Close() + assert.NoError(t, err) + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + // Check that actions are migrated. + action1Get, _, err := httpdtest.GetEventActionByName(action1.Name, http.StatusOK) + assert.NoError(t, err) + action2Get, _, err := httpdtest.GetEventActionByName(action2.Name, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, `Failed "{{.Event}}" from "{{.Name}}"`, action1Get.Options.EmailConfig.Subject) + assert.Equal(t, `Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}`, action1Get.Options.EmailConfig.Body) + assert.Equal(t, `/{{.VirtualDirPath}}/{{.ObjectName}}`, action2Get.Options.FsConfig.Renames[0].Key) + assert.Equal(t, `/{{.ObjectName}}_renamed`, action2Get.Options.FsConfig.Renames[0].Value) + // Clenup. + _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action2, http.StatusOK) + assert.NoError(t, err) +} + func TestBasicUserHandling(t *testing.T) { u := getTestUser() u.Email = "user@user.com" @@ -1863,9 +1929,9 @@ func TestBasicActionRulesHandling(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"email@example.com"}, Bcc: []string{"bcc@example.com"}, - Subject: "Event: {{Event}}", + Subject: "Event: {{.Event}}", Body: "test mail body", - Attachments: []string{"/{{VirtualPath}}"}, + Attachments: []string{"/{{.VirtualPath}}"}, }, } @@ -1903,7 +1969,7 @@ func TestBasicActionRulesHandling(t *testing.T) { Value: "b", }, }, - Body: `{"event":"{{Event}}","name":"{{Name}}"}`, + Body: `{"event":"{{.Event}}","name":"{{.Name}}"}`, }, } action, _, err = httpdtest.UpdateEventAction(a, http.StatusOK) @@ -8362,7 +8428,7 @@ func TestLoaddata(t *testing.T) { Timeout: 10, SkipTLSVerify: true, Method: http.MethodPost, - Body: `{"event":"{{Event}}","name":"{{Name}}"}`, + Body: `{"event":"{{.Event}}","name":"{{.Name}}"}`, }, }, } @@ -8608,6 +8674,85 @@ func TestLoaddata(t *testing.T) { assert.NoError(t, err) } +func TestLoaddataConvertActions(t *testing.T) { + a1 := dataprovider.BaseEventAction{ + Name: xid.New().String(), + Type: dataprovider.ActionTypeEmail, + Options: dataprovider.BaseEventActionOptions{ + EmailConfig: dataprovider.EventActionEmailConfig{ + Recipients: []string{"failure@example.com"}, + Subject: `Failed "{{Event}}" from "{{Name}}"`, + Body: "Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}", + }, + }, + } + a2 := dataprovider.BaseEventAction{ + Name: xid.New().String(), + Type: dataprovider.ActionTypeFilesystem, + Options: dataprovider.BaseEventActionOptions{ + FsConfig: dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionRename, + Renames: []dataprovider.RenameConfig{ + { + KeyValue: dataprovider.KeyValue{ + Key: "/{{VirtualDirPath}}/{{ObjectName}}", + Value: "/{{ObjectName}}_renamed", + }, + }, + }, + }, + }, + } + backupData := dataprovider.BackupData{ + EventActions: []dataprovider.BaseEventAction{a1, a2}, + Version: 16, + } + backupContent, err := json.Marshal(backupData) + assert.NoError(t, err) + backupFilePath := filepath.Join(backupsPath, "backup.json") + err = os.WriteFile(backupFilePath, backupContent, os.ModePerm) + assert.NoError(t, err) + _, resp, err := httpdtest.Loaddata(backupFilePath, "1", "2", http.StatusOK) + assert.NoError(t, err, string(resp)) + // Check that actions are migrated. + action1, _, err := httpdtest.GetEventActionByName(a1.Name, http.StatusOK) + assert.NoError(t, err) + action2, _, err := httpdtest.GetEventActionByName(a2.Name, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, `Failed "{{.Event}}" from "{{.Name}}"`, action1.Options.EmailConfig.Subject) + assert.Equal(t, `Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}`, action1.Options.EmailConfig.Body) + assert.Equal(t, `/{{.VirtualDirPath}}/{{.ObjectName}}`, action2.Options.FsConfig.Renames[0].Key) + assert.Equal(t, `/{{.ObjectName}}_renamed`, action2.Options.FsConfig.Renames[0].Value) + // If we restore a backup from the current version actions are not migrated. + backupData = dataprovider.BackupData{ + EventActions: []dataprovider.BaseEventAction{a1, a2}, + Version: dataprovider.DumpVersion, + } + backupContent, err = json.Marshal(backupData) + assert.NoError(t, err) + backupFilePath = filepath.Join(backupsPath, "backup.json") + err = os.WriteFile(backupFilePath, backupContent, os.ModePerm) + assert.NoError(t, err) + _, resp, err = httpdtest.Loaddata(backupFilePath, "1", "2", http.StatusOK) + assert.NoError(t, err, string(resp)) + action1, _, err = httpdtest.GetEventActionByName(a1.Name, http.StatusOK) + assert.NoError(t, err) + action2, _, err = httpdtest.GetEventActionByName(a2.Name, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, `Failed "{{Event}}" from "{{Name}}"`, action1.Options.EmailConfig.Subject) + assert.Equal(t, `Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}`, action1.Options.EmailConfig.Body) + assert.Equal(t, `/{{VirtualDirPath}}/{{ObjectName}}`, action2.Options.FsConfig.Renames[0].Key) + assert.Equal(t, `/{{ObjectName}}_renamed`, action2.Options.FsConfig.Renames[0].Value) + // Cleanup. + _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action2, http.StatusOK) + assert.NoError(t, err) + actions, _, err := httpdtest.GetEventActions(0, 0, http.StatusOK) + assert.NoError(t, err) + assert.Len(t, actions, 0) +} + func TestLoaddataMode(t *testing.T) { err := dataprovider.UpdateConfigs(nil, "", "", "") assert.NoError(t, err) @@ -8665,7 +8810,7 @@ func TestLoaddataMode(t *testing.T) { Timeout: 10, SkipTLSVerify: true, Method: http.MethodPost, - Body: `{"event":"{{Event}}","name":"{{Name}}"}`, + Body: `{"event":"{{.Event}}","name":"{{.Name}}"}`, }, }, } @@ -23831,7 +23976,7 @@ func TestWebEventAction(t *testing.T) { Value: "value1", }, }, - Body: `{"event":"{{Event}}","name":"{{Name}}"}`, + Body: `{"event":"{{.Event}}","name":"{{.Name}}"}`, }, }, } @@ -23950,12 +24095,12 @@ func TestWebEventAction(t *testing.T) { form.Del("http_headers[0][http_header_key]") form.Del("http_headers[0][http_header_val]") form.Set("multipart_body[0][http_part_name]", "part1") - form.Set("multipart_body[0][http_part_file]", "{{VirtualPath}}") + form.Set("multipart_body[0][http_part_file]", "{{.VirtualPath}}") form.Set("multipart_body[0][http_part_body]", "") form.Set("multipart_body[0][http_part_headers]", "X-MyHeader: a:b,c") form.Set("multipart_body[12][http_part_name]", "part2") form.Set("multipart_body[12][http_part_headers]", "Content-Type:application/json \r\n") - form.Set("multipart_body[12][http_part_body]", "{{ObjectData}}") + form.Set("multipart_body[12][http_part_body]", "{{.ObjectData}}") req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name), bytes.NewBuffer([]byte(form.Encode()))) assert.NoError(t, err) @@ -23972,12 +24117,12 @@ func TestWebEventAction(t *testing.T) { assert.Equal(t, 0, dbAction.Options.HTTPConfig.Timeout) if assert.Len(t, dbAction.Options.HTTPConfig.Parts, 2) { assert.Equal(t, "part1", dbAction.Options.HTTPConfig.Parts[0].Name) - assert.Equal(t, "/{{VirtualPath}}", dbAction.Options.HTTPConfig.Parts[0].Filepath) + assert.Equal(t, "/{{.VirtualPath}}", dbAction.Options.HTTPConfig.Parts[0].Filepath) assert.Empty(t, dbAction.Options.HTTPConfig.Parts[0].Body) assert.Equal(t, "X-MyHeader", dbAction.Options.HTTPConfig.Parts[0].Headers[0].Key) assert.Equal(t, "a:b,c", dbAction.Options.HTTPConfig.Parts[0].Headers[0].Value) assert.Equal(t, "part2", dbAction.Options.HTTPConfig.Parts[1].Name) - assert.Equal(t, "{{ObjectData}}", dbAction.Options.HTTPConfig.Parts[1].Body) + assert.Equal(t, "{{.ObjectData}}", dbAction.Options.HTTPConfig.Parts[1].Body) assert.Empty(t, dbAction.Options.HTTPConfig.Parts[1].Filepath) assert.Equal(t, "Content-Type", dbAction.Options.HTTPConfig.Parts[1].Headers[0].Key) assert.Equal(t, "application/json", dbAction.Options.HTTPConfig.Parts[1].Headers[0].Value) diff --git a/internal/httpd/oidc_test.go b/internal/httpd/oidc_test.go index c8848ca0..cb0a4fc3 100644 --- a/internal/httpd/oidc_test.go +++ b/internal/httpd/oidc_test.go @@ -1182,18 +1182,18 @@ func TestOIDCEvMgrIntegration(t *testing.T) { // add a special chars to check json replacer username := `test_"oidc_eventmanager` u := map[string]any{ - "username": "{{Name}}", + "username": "{{.Name}}", "status": 1, - "home_dir": filepath.Join(os.TempDir(), "{{IDPFieldcustom1.sub}}"), + "home_dir": filepath.Join(os.TempDir(), "{{.IDPFieldcustom1.sub}}"), "permissions": map[string][]string{ "/": {dataprovider.PermAny}, }, - "description": "{{IDPFieldcustom2}}", + "description": "{{.IDPFieldcustom2}}", } userTmpl, err := json.Marshal(u) require.NoError(t, err) a := map[string]any{ - "username": "{{Name}}", + "username": "{{.Name}}", "status": 1, "permissions": []string{dataprovider.PermAdminAny}, } diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index 01a7b33e..c50f6b0c 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -889,115 +889,115 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).