Explorar o código

EventManager: add datetime placeholder

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino hai 1 ano
pai
achega
4103344989

+ 1 - 1
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

+ 8 - 5
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,
 	}

+ 12 - 11
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)

+ 1 - 1
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,
 			})
 		}

+ 1 - 1
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 {

+ 12 - 4
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),

+ 22 - 0
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{

+ 7 - 5
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{"[email protected]"},
 				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, "[email protected]"))
 	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,
 	})

+ 1 - 1
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() {

+ 1 - 0
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",

+ 1 - 0
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",

+ 3 - 0
templates/webadmin/eventaction.html

@@ -941,6 +941,9 @@ explicit grant from the SFTPGo Team ([email protected]).
                 <p>
                     <span class="shortcut">{{`{{Timestamp}}`}}</span> => <span data-i18n="actions.placeholders_modal.timestamp">Event timestamp as nanoseconds since epoch.</span>
                 </p>
+                <p>
+                    <span class="shortcut">{{`{{DateTime}}`}}</span> => <span data-i18n="actions.placeholders_modal.datetime">Timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ.</span>
+                </p>
                 <p>
                     <span class="shortcut">{{`{{Email}}`}}</span> => <span data-i18n="actions.placeholders_modal.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.</span>
                 </p>