From 41033449895ea015c603edfad34373fee34e056c Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Tue, 8 Oct 2024 18:39:00 +0200 Subject: [PATCH] EventManager: add datetime placeholder Signed-off-by: Nicola Murino --- internal/acme/acme.go | 2 +- internal/common/actions.go | 13 ++++++++----- internal/common/actions_test.go | 23 ++++++++++++----------- internal/common/defenderdb.go | 2 +- internal/common/defendermem.go | 2 +- internal/common/eventmanager.go | 16 ++++++++++++---- internal/common/eventmanager_test.go | 22 ++++++++++++++++++++++ internal/common/protocol_test.go | 12 +++++++----- internal/httpd/oidc.go | 2 +- static/locales/en/translation.json | 1 + static/locales/it/translation.json | 1 + templates/webadmin/eventaction.html | 3 +++ 12 files changed, 70 insertions(+), 29 deletions(-) diff --git a/internal/acme/acme.go b/internal/acme/acme.go index 347375e1..8506f721 100644 --- a/internal/acme/acme.go +++ b/internal/acme/acme.go @@ -673,7 +673,7 @@ func (c *Configuration) notifyCertificateRenewal(domain string, err error) { params := common.EventParams{ Name: domain, Event: "Certificate renewal", - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), } if err != nil { params.Status = 2 diff --git a/internal/common/actions.go b/internal/common/actions.go index 2f8c39f2..1da74b8a 100644 --- a/internal/common/actions.go +++ b/internal/common/actions.go @@ -92,8 +92,9 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str if !hasHook && !hasNotifiersPlugin && !hasRules { return 0, nil } + dateTime := time.Now() event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "", - conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, nil) + conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, dateTime, nil) if hasNotifiersPlugin { plugin.Handler.NotifyFsEvent(event) } @@ -113,7 +114,7 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str Protocol: event.Protocol, IP: event.IP, Role: event.Role, - Timestamp: event.Timestamp, + Timestamp: dateTime, Email: conn.User.Email, Object: nil, } @@ -138,8 +139,9 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua if !hasHook && !hasNotifiersPlugin && !hasRules { return nil } + dateTime := time.Now() notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd, - conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, metadata) + conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, dateTime, metadata) if hasNotifiersPlugin { plugin.Handler.NotifyFsEvent(notification) } @@ -160,7 +162,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua Protocol: notification.Protocol, IP: notification.IP, Role: notification.Role, - Timestamp: notification.Timestamp, + Timestamp: dateTime, Email: conn.User.Email, Object: nil, Metadata: metadata, @@ -198,6 +200,7 @@ func newActionNotification( operation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol, ip, sessionID string, fileSize int64, openFlags, status int, elapsed int64, + datetime time.Time, metadata map[string]string, ) *notifier.FsEvent { var bucket, endpoint string @@ -239,7 +242,7 @@ func newActionNotification( SessionID: sessionID, OpenFlags: openFlags, Role: user.Role, - Timestamp: time.Now().UnixNano(), + Timestamp: datetime.UnixNano(), Elapsed: elapsed, Metadata: metadata, } diff --git a/internal/common/actions_test.go b/internal/common/actions_test.go index 87b168f6..6ac0463c 100644 --- a/internal/common/actions_test.go +++ b/internal/common/actions_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "runtime" "testing" + "time" "github.com/lithammer/shortuuid/v4" "github.com/rs/xid" @@ -71,7 +72,7 @@ func TestNewActionNotification(t *testing.T) { c := NewBaseConnection("id", ProtocolSSH, "", "", user) sessionID := xid.New().String() a := newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID, - 123, 0, c.getNotificationStatus(errors.New("fake error")), 0, nil) + 123, 0, c.getNotificationStatus(errors.New("fake error")), 0, time.Now(), nil) assert.Equal(t, user.Username, a.Username) assert.Equal(t, 0, len(a.Bucket)) assert.Equal(t, 0, len(a.Endpoint)) @@ -79,38 +80,38 @@ func TestNewActionNotification(t *testing.T) { user.FsConfig.Provider = sdk.S3FilesystemProvider a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID, - 123, 0, c.getNotificationStatus(nil), 0, nil) + 123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil) assert.Equal(t, "s3bucket", a.Bucket) assert.Equal(t, "endpoint", a.Endpoint) assert.Equal(t, 1, a.Status) user.FsConfig.Provider = sdk.GCSFilesystemProvider a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID, - 123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, nil) + 123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, time.Now(), nil) assert.Equal(t, "gcsbucket", a.Bucket) assert.Equal(t, 0, len(a.Endpoint)) assert.Equal(t, 3, a.Status) a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID, - 123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, nil) + 123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, time.Now(), nil) assert.Equal(t, "gcsbucket", a.Bucket) assert.Equal(t, 0, len(a.Endpoint)) assert.Equal(t, 3, a.Status) user.FsConfig.Provider = sdk.HTTPFilesystemProvider a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID, - 123, 0, c.getNotificationStatus(nil), 0, nil) + 123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil) assert.Equal(t, "httpendpoint", a.Endpoint) assert.Equal(t, 1, a.Status) user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID, - 123, 0, c.getNotificationStatus(nil), 0, nil) + 123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil) assert.Equal(t, "azcontainer", a.Bucket) assert.Equal(t, "azendpoint", a.Endpoint) assert.Equal(t, 1, a.Status) a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID, - 123, os.O_APPEND, c.getNotificationStatus(nil), 0, nil) + 123, os.O_APPEND, c.getNotificationStatus(nil), 0, time.Now(), nil) assert.Equal(t, "azcontainer", a.Bucket) assert.Equal(t, "azendpoint", a.Endpoint) assert.Equal(t, 1, a.Status) @@ -118,7 +119,7 @@ func TestNewActionNotification(t *testing.T) { user.FsConfig.Provider = sdk.SFTPFilesystemProvider a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID, - 123, 0, c.getNotificationStatus(nil), 0, nil) + 123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil) assert.Equal(t, "sftpendpoint", a.Endpoint) } @@ -135,7 +136,7 @@ func TestActionHTTP(t *testing.T) { }, } a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", - xid.New().String(), 123, 0, 1, 0, nil) + xid.New().String(), 123, 0, 1, 0, time.Now(), nil) status, err := actionHandler.Handle(a) assert.NoError(t, err) assert.Equal(t, 1, status) @@ -175,7 +176,7 @@ func TestActionCMD(t *testing.T) { } sessionID := shortuuid.New() a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID, - 123, 0, 1, 0, map[string]string{"key": "value"}) + 123, 0, 1, 0, time.Now(), map[string]string{"key": "value"}) status, err := actionHandler.Handle(a) assert.NoError(t, err) assert.Equal(t, 1, status) @@ -208,7 +209,7 @@ func TestWrongActions(t *testing.T) { } a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", xid.New().String(), - 123, 0, 1, 0, nil) + 123, 0, 1, 0, time.Now(), nil) status, err := actionHandler.Handle(a) assert.Error(t, err, "action with bad command must fail") assert.Equal(t, 1, status) diff --git a/internal/common/defenderdb.go b/internal/common/defenderdb.go index d60879b4..3f0a4849 100644 --- a/internal/common/defenderdb.go +++ b/internal/common/defenderdb.go @@ -110,7 +110,7 @@ func (d *dbDefender) AddEvent(ip, protocol string, event HostEvent) bool { eventManager.handleIPBlockedEvent(EventParams{ Event: ipBlockedEventName, IP: ip, - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, }) } diff --git a/internal/common/defendermem.go b/internal/common/defendermem.go index 6a908d04..b5642c4e 100644 --- a/internal/common/defendermem.go +++ b/internal/common/defendermem.go @@ -218,7 +218,7 @@ func (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool { eventManager.handleIPBlockedEvent(EventParams{ Event: ipBlockedEventName, IP: ip, - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, }) } else { diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index eb4b3a2a..5e79efa0 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -58,6 +58,7 @@ const ( maxAttachmentsSize = int64(10 * 1024 * 1024) objDataPlaceholder = "{{ObjectData}}" objDataPlaceholderString = "{{ObjectDataString}}" + dateTimeMillisFormat = "2006-01-02T15:04:05.000" ) // Supported IDP login events @@ -89,7 +90,7 @@ func init() { ObjectType: objectType, IP: ip, Role: role, - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Object: object, } if u, ok := object.(*dataprovider.User); ok { @@ -557,7 +558,7 @@ type EventParams struct { IP string Role string Email string - Timestamp int64 + Timestamp time.Time UID string IDPCustomFields *map[string]string Object plugin.Renderer @@ -641,7 +642,7 @@ func (p *EventParams) setBackupParams(backupPath string) { p.FsPath = backupPath p.ObjectName = filepath.Base(backupPath) p.VirtualPath = "/" + p.ObjectName - p.Timestamp = time.Now().UnixNano() + p.Timestamp = time.Now() info, err := os.Stat(backupPath) if err == nil { p.FileSize = info.Size() @@ -775,6 +776,12 @@ func (*EventParams) getStringReplacement(val string, jsonEscaped bool) string { } func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []string { + var dateTimeString string + if Config.TZ == "local" { + dateTimeString = p.Timestamp.Local().Format(dateTimeMillisFormat) + } else { + dateTimeString = p.Timestamp.UTC().Format(dateTimeMillisFormat) + } replacements := []string{ "{{Name}}", p.getStringReplacement(p.Name, jsonEscaped), "{{Event}}", p.Event, @@ -791,7 +798,8 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s "{{IP}}", p.IP, "{{Role}}", p.getStringReplacement(p.Role, jsonEscaped), "{{Email}}", p.getStringReplacement(p.Email, jsonEscaped), - "{{Timestamp}}", strconv.FormatInt(p.Timestamp, 10), + "{{Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10), + "{{DateTime}}", dateTimeString, "{{StatusString}}", p.getStatusString(), "{{UID}}", p.getStringReplacement(p.UID, jsonEscaped), "{{Ext}}", p.getStringReplacement(p.Extension, jsonEscaped), diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index f00330b1..f23a31cc 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -800,6 +800,28 @@ func TestEventManagerErrors(t *testing.T) { stopEventScheduler() } +func TestDateTimePlaceholder(t *testing.T) { + oldTZ := Config.TZ + + Config.TZ = "" + dateTime := time.Now() + params := EventParams{ + Timestamp: dateTime, + } + replacements := params.getStringReplacements(false, false) + r := strings.NewReplacer(replacements...) + res := r.Replace("{{DateTime}}") + assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res) + + Config.TZ = "local" + replacements = params.getStringReplacements(false, false) + r = strings.NewReplacer(replacements...) + res = r.Replace("{{DateTime}}") + assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res) + + Config.TZ = oldTZ +} + func TestEventRuleActions(t *testing.T) { actionName := "test rule action" action := dataprovider.BaseEventAction{ diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 1e0fcb7e..840fb374 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -5431,7 +5431,7 @@ func TestBackupAsAttachment(t *testing.T) { common.HandleCertificateEvent(common.EventParams{ Name: "example.com", - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, Event: renewalEvent, }) @@ -7108,7 +7108,7 @@ func TestEventRuleCertificate(t *testing.T) { Recipients: []string{"test@example.com"}, Subject: `"{{Event}} {{StatusString}}"`, ContentType: 0, - Body: "Domain: {{Name}} Timestamp: {{Timestamp}} {{ErrorString}}", + Body: "Domain: {{Name}} Timestamp: {{Timestamp}} {{ErrorString}} Date time: {{DateTime}}", }, }, } @@ -7163,7 +7163,7 @@ func TestEventRuleCertificate(t *testing.T) { common.HandleCertificateEvent(common.EventParams{ Name: "example.com", - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, Event: renewalEvent, }) @@ -7178,9 +7178,10 @@ func TestEventRuleCertificate(t *testing.T) { assert.Contains(t, email.Data, `Domain: example.com Timestamp`) lastReceivedEmail.reset() + dateTime := time.Now() params := common.EventParams{ Name: "example.com", - Timestamp: time.Now().UnixNano(), + Timestamp: dateTime, Status: 2, Event: renewalEvent, } @@ -7195,6 +7196,7 @@ func TestEventRuleCertificate(t *testing.T) { assert.True(t, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent)) assert.Contains(t, email.Data, `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, dateTime.UTC().Format("2006-01-02T15:04:05.000")) assert.Contains(t, email.Data, errRenew.Error()) _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) @@ -7208,7 +7210,7 @@ func TestEventRuleCertificate(t *testing.T) { // ignored no more certificate rules common.HandleCertificateEvent(common.EventParams{ Name: "example.com", - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, Event: renewalEvent, }) diff --git a/internal/httpd/oidc.go b/internal/httpd/oidc.go index 19af66b3..2ab22137 100644 --- a/internal/httpd/oidc.go +++ b/internal/httpd/oidc.go @@ -408,7 +408,7 @@ func (t *oidcToken) getUser(r *http.Request) error { Name: t.Username, IP: ipAddr, Protocol: common.ProtocolOIDC, - Timestamp: time.Now().UnixNano(), + Timestamp: time.Now(), Status: 1, } if t.isAdmin() { diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 6db16b35..33cdc1b3 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -1057,6 +1057,7 @@ "ip": "Client IP address", "role": "User or admin role", "timestamp": "Event timestamp as nanoseconds since epoch", + "datetime": "Event timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ", "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", "object_data": "Provider object data serialized as JSON with sensitive fields removed", "object_data_string": "Provider object data as JSON escaped string with sensitive fields removed", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index de5df0d6..c8a2f4a6 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -1057,6 +1057,7 @@ "ip": "Indirizzo IP del client", "role": "Ruolo dell'utente o dell'amministratore", "timestamp": "Timestamp dell'evento in nanosecondi dall'epoch time", + "datetime": "Timestamp dell'evento formattato come YYYY-MM-DDTHH:MM:SS.ZZZ", "email": "Per gli eventi del file system, questa รจ l'e-mail associata all'utente che esegue l'azione. Per gli eventi del provider, si tratta dell'e-mail associata all'utente o all'amministratore interessato. Vuoto in tutti gli altri casi", "object_data": "Dati dell'oggetto provider serializzati come JSON con campi sensibili rimossi", "object_data_string": "Dati dell'oggetto provider serializzati come stringa JSON escaped con campi sensibili rimossi", diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index a2b77bca..3773a88d 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -941,6 +941,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).

{{`{{Timestamp}}`}} => Event timestamp as nanoseconds since epoch.

+

+ {{`{{DateTime}}`}} => Timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ. +

{{`{{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.