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).- {{`{{Name}}`}} => Username, folder name, admin username for provider events, domain name for certificate events. + {{`{{.Name}}`}} => Username, folder name, admin username for provider events, domain name for certificate events.
- {{`{{Event}}`}} => Event name, for example "upload", "download" for filesystem events or "add", "update" for provider events. + {{`{{.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. + {{`{{.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". + {{`{{.StatusString}}`}} => Status as string. Possible values "OK", "KO".
- {{`{{ErrorString}}`}} => Error details. Replaced with an empty string if no errors occur. + {{`{{.ErrorString}}`}} => Error details. Replaced with an empty string if no errors occur.
- {{`{{VirtualPath}}`}} => Path seen by SFTPGo users, for example "/adir/afile.txt". + {{`{{.VirtualPath}}`}} => Path seen by SFTPGo users, for example "/adir/afile.txt".
- {{`{{EscapedVirtualPath}}`}} => HTTP query string encoded path, for example "%2Fadir%2Fafile.txt". + {{`{{.EscapedVirtualPath}}`}} => HTTP query string encoded path, for example "%2Fadir%2Fafile.txt".
- {{`{{VirtualDirPath}}`}} => Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir". + {{`{{.VirtualDirPath}}`}} => Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir".
- {{`{{FsPath}}`}} => Full filesystem path, for example "/user/homedir/adir/afile.txt" or "C:/data/user/homedir/adir/afile.txt" on Windows. + {{`{{.FsPath}}`}} => Full filesystem path, for example "/user/homedir/adir/afile.txt" or "C:/data/user/homedir/adir/afile.txt" on Windows.
- {{`{{Ext}}`}} => File extension, for example ".txt" if the filename is "afile.txt". + {{`{{.Ext}}`}} => File extension, for example ".txt" if the filename is "afile.txt".
- {{`{{ObjectName}}`}} => File/directory name, for example "afile.txt" or provider object name. + {{`{{.ObjectName}}`}} => File/directory name, for example "afile.txt" or provider object name.
- {{`{{ObjectBaseName}}`}} => Filename without extension, for example "afile" if the filename is "afile.txt". + {{`{{.ObjectBaseName}}`}} => Filename without extension, for example "afile" if the filename is "afile.txt".
- {{`{{ObjectType}}`}} => Object type for provider events: "user", "group", "admin", etc. + {{`{{.ObjectType}}`}} => Object type for provider events: "user", "group", "admin", etc.
- {{`{{VirtualTargetPath}}`}} => Virtual target path for renames. + {{`{{.VirtualTargetPath}}`}} => Virtual target path for renames.
- {{`{{VirtualTargetDirPath}}`}} => Parent directory for VirtualTargetPath. + {{`{{.VirtualTargetDirPath}}`}} => Parent directory for VirtualTargetPath.
- {{`{{TargetName}}`}} => Target object name for renames. + {{`{{.TargetName}}`}} => Target object name for renames.
- {{`{{FsTargetPath}}`}} => Full filesystem target path for renames. + {{`{{.FsTargetPath}}`}} => Full filesystem target path for renames.
- {{`{{FileSize}}`}} => File size. + {{`{{.FileSize}}`}} => File size.
- {{`{{Elapsed}}`}} => Elapsed time as milliseconds for filesystem events. + {{`{{.Elapsed}}`}} => Elapsed time as milliseconds for filesystem events.
- {{`{{Protocol}}`}} => Protocol, for example "SFTP", "FTP". + {{`{{.Protocol}}`}} => Protocol, for example "SFTP", "FTP".
- {{`{{IP}}`}} => Client IP address. + {{`{{.IP}}`}} => Client IP address.
- {{`{{Role}}`}} => User or admin role. + {{`{{.Role}}`}} => User or admin role.
- {{`{{Timestamp}}`}} => Event timestamp as nanoseconds since epoch. + {{`{{.Timestamp}}`}} => Event timestamp as nanoseconds since epoch.
- {{`{{DateTime}}`}} => Timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ. + {{`{{.DateTime}}`}} => Timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ.
- {{`{{Year}}`}} => Event year formatted as four digits. + {{`{{.Year}}`}} => Event year formatted as four digits.
- {{`{{Month}}`}} => Event month formatted as two digits. + {{`{{.Month}}`}} => Event month formatted as two digits.
- {{`{{Day}}`}} => Event day formatted as two digits. + {{`{{.Day}}`}} => Event day formatted as two digits.
- {{`{{Hour}}`}} => Event hour formatted as two digits. + {{`{{.Hour}}`}} => Event hour formatted as two digits.
- {{`{{Minute}}`}} => Event minute formatted as two digits. + {{`{{.Minute}}`}} => Event minute formatted as two digits.
- {{`{{Email}}`}} => For filesystem events, this is the email associated with the user performing the action. For the provider events, this is the email associated with the affected user or admin. Blank in all other cases. + {{`{{.Email}}`}} => For filesystem events, this is the email associated with the user performing the action. For the provider events, this is the email associated with the affected user or admin. Blank in all other cases.
- {{`{{ObjectData}}`}} => Provider object data serialized as JSON with sensitive fields removed. + {{`{{.ObjectData}}`}} => Provider object data serialized as JSON with sensitive fields removed.
- {{`{{ObjectDataString}}`}} => Provider object data as JSON escaped string with sensitive fields removed. + {{`{{.ObjectDataString}}`}} => Provider object data as JSON escaped string with sensitive fields removed.
- {{`{{RetentionReports}}`}} => Data retention reports as zip compressed CSV files. Supported as email attachment, file path for multipart HTTP request and as single parameter for HTTP requests body. + {{`{{.RetentionReports}}`}} => Data retention reports as zip compressed CSV files. Supported as email attachment, file path for multipart HTTP request and as single parameter for HTTP requests body.
- {{`{{IDPField
- {{`{{Metadata}}`}} => Cloud storage metadata for the downloaded file serialized as JSON. + {{`{{.Metadata}}`}} => Cloud storage metadata for the downloaded file serialized as JSON.
- {{`{{MetadataString}}`}} => Cloud storage metadata for the downloaded file as JSON escaped string. + {{`{{.MetadataString}}`}} => Cloud storage metadata for the downloaded file as JSON escaped string.
- {{`{{UID}}`}} => Unique ID. + {{`{{.UID}}`}} => Unique ID.