Browse Source

notifier plugin: add support for login succeeded events

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 year ago
parent
commit
456517af87

+ 1 - 1
docs/full-configuration.md

@@ -482,7 +482,7 @@ The configuration file contains the following sections:
     - `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
     - `provider_events`, list of strings. Defines the provider events that will be notified to this plugin.
     - `provider_objects`, list if strings. Defines the provider objects that will be notified to this plugin.
-    - `log_events`, list of integers. Defines the log events that will be notified to this plugin. `1` means "Login failed", `2` means "Login with non-existent user", `3` means "No login tried", `4` means "Algorithm negotiation failed".
+    - `log_events`, list of integers. Defines the log events that will be notified to this plugin. `1` means "Login failed", `2` means "Login with non-existent user", `3` means "No login tried", `4` means "Algorithm negotiation failed", `5` means "Login succeeded".
     - `retry_max_time`, integer. Defines the maximum number of seconds an event can be late. SFTPGo adds a timestamp to each event and add to an internal queue any events that a the plugin fails to handle (the plugin returns an error or it is not running). If a plugin fails to handle an event that is too late, based on this configuration, it will be discarded. SFTPGo will try to resend queued events every 30 seconds. 0 means no retry.
     - `retry_queue_max_size`, integer. Defines the maximum number of events that the internal queue can hold. Once the queue is full, the events that cannot be sent to the plugin will be discarded. 0 means no limit.
   - `kms_options`, struct. Defines the options for kms plugins.

+ 2 - 3
go.mod

@@ -46,14 +46,14 @@ require (
 	github.com/minio/sio v0.3.1
 	github.com/otiai10/copy v1.14.0
 	github.com/pires/go-proxyproto v0.7.0
-	github.com/pkg/sftp v1.13.6
+	github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317
 	github.com/pquerna/otp v1.4.0
 	github.com/prometheus/client_golang v1.19.0
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/rs/cors v1.10.1
 	github.com/rs/xid v1.5.0
 	github.com/rs/zerolog v1.32.0
-	github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3
+	github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896
 	github.com/shirou/gopsutil/v3 v3.24.3
 	github.com/spf13/afero v1.11.0
 	github.com/spf13/cobra v1.8.0
@@ -185,7 +185,6 @@ require (
 replace (
 	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240313174824-cf52df3aa8f7
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240210102745-f1ffc43f78d2
-	github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20240214104840-fbb0b8bdb30c
 	github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240405104909-a6b14455cac6
 )

+ 4 - 0
go.sum

@@ -314,6 +314,8 @@ github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317 h1:kupFhKi4R3XqKmUmqGSHWn/WZbC9CnwSoW421tL1gGw=
+github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -353,6 +355,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3 h1:svxTNm3r2kRlpuVSUKi0WKQlsAq8VI0EzDWPNqeNn/o=
 github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
+github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896 h1:ykxybS9WKurHTatKJ9WjqYD+WH9YH/2QMxCkxUPTVLY=
+github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
 github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
 github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=

+ 3 - 1
internal/ftpd/server.go

@@ -415,7 +415,9 @@ func setStartDirectory(startDirectory string, cc ftpserver.ClientContext) {
 
 func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
 	metric.AddLoginAttempt(loginMethod)
-	if err != nil && err != common.ErrInternalFailure {
+	if err == nil {
+		plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolFTP, user.Username, ip, "", nil)
+	} else if err != common.ErrInternalFailure {
 		logger.ConnectionFailedLog(user.Username, ip, loginMethod,
 			common.ProtocolFTP, err.Error())
 		event := common.HostEventLoginFailed

+ 2 - 0
internal/httpd/api_events.go

@@ -472,6 +472,8 @@ func getLogEventString(event notifier.LogEventType) string {
 		return "No login tried"
 	case notifier.LogEventTypeNotNegotiated:
 		return "Algorithm negotiation failed"
+	case notifier.LogEventTypeLoginOK:
+		return "Login succeeded"
 	default:
 		return ""
 	}

+ 3 - 1
internal/httpd/api_utils.go

@@ -697,7 +697,9 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
 	default:
 		protocol = common.ProtocolHTTP
 	}
-	if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
+	if err == nil {
+		plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, protocol, user.Username, ip, "", nil)
+	} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
 		logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
 		err = handleDefenderEventLoginFailed(ip, err)
 		logEv := notifier.LogEventTypeLoginFailed

+ 1 - 0
internal/httpd/internal_test.go

@@ -3398,6 +3398,7 @@ func TestGetLogEventString(t *testing.T) {
 	assert.Equal(t, "Login with non-existent user", getLogEventString(notifier.LogEventTypeLoginNoUser))
 	assert.Equal(t, "No login tried", getLogEventString(notifier.LogEventTypeNoLoginTried))
 	assert.Equal(t, "Algorithm negotiation failed", getLogEventString(notifier.LogEventTypeNotNegotiated))
+	assert.Equal(t, "Login succeeded", getLogEventString(notifier.LogEventTypeLoginOK))
 	assert.Empty(t, getLogEventString(0))
 }
 

+ 3 - 4
internal/plugin/notifier.go

@@ -44,6 +44,9 @@ func (c *NotifierConfig) hasActions() bool {
 	if len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 {
 		return true
 	}
+	if len(c.LogEvents) > 0 {
+		return true
+	}
 	return false
 }
 
@@ -250,10 +253,6 @@ func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, obj
 }
 
 func (p *notifierPlugin) notifyLogEvent(event *notifier.LogEvent) {
-	if !util.Contains(p.config.NotifierOptions.LogEvents, int(event.Event)) {
-		return
-	}
-
 	go func() {
 		Handler.addTask()
 		defer Handler.removeTask()

+ 20 - 10
internal/plugin/plugin.go

@@ -331,18 +331,28 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username
 	m.notifLock.RLock()
 	defer m.notifLock.RUnlock()
 
-	e := &notifier.LogEvent{
-		Timestamp: time.Now().UnixNano(),
-		Event:     event,
-		Protocol:  protocol,
-		Username:  username,
-		IP:        ip,
-		Message:   err.Error(),
-		Role:      role,
-	}
+	var e *notifier.LogEvent
 
 	for _, n := range m.notifiers {
-		n.notifyLogEvent(e)
+		if util.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
+			if e == nil {
+				message := ""
+				if err != nil {
+					message = err.Error()
+				}
+
+				e = &notifier.LogEvent{
+					Timestamp: time.Now().UnixNano(),
+					Event:     event,
+					Protocol:  protocol,
+					Username:  username,
+					IP:        ip,
+					Message:   message,
+					Role:      role,
+				}
+			}
+			n.notifyLogEvent(e)
+		}
 	}
 }
 

+ 3 - 1
internal/sftpd/server.go

@@ -1248,7 +1248,9 @@ func (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMeta
 
 func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
 	metric.AddLoginAttempt(method)
-	if err != nil {
+	if err == nil {
+		plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolSSH, user.Username, ip, "", err)
+	} else {
 		logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())
 		if method != dataprovider.SSHLoginMethodPublicKey {
 			// some clients try all available public keys for a user, we

+ 3 - 1
internal/webdavd/server.go

@@ -422,7 +422,9 @@ func writeLog(r *http.Request, status int, err error) {
 
 func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
 	metric.AddLoginAttempt(loginMethod)
-	if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
+	if err == nil {
+		plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolWebDAV, user.Username, ip, "", nil)
+	} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
 		logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
 		event := common.HostEventLoginFailed
 		logEv := notifier.LogEventTypeLoginFailed

+ 2 - 0
openapi/openapi.yaml

@@ -5214,12 +5214,14 @@ components:
         - 2
         - 3
         - 4
+        - 5
       description: >
         Event status:
           * `1` - Login failed
           * `2` - Login failed non-existent user
           * `3` - No login tried
           * `4` - Algorithm negotiation failed
+          * `5` - Login succeeded
     FsEventStatus:
       type: integer
       enum:

+ 1 - 0
static/locales/en/translation.json

@@ -900,6 +900,7 @@
         "add": "Addition",
         "update": "Update",
         "login_failed": "Login failed",
+        "login_ok": "Login succeeded",
         "login_missing_user": "Login with non-existent user",
         "no_login_tried": "No login tried",
         "algo_negotiation_failed": "Algorithm negotiation failed",

+ 1 - 0
static/locales/it/translation.json

@@ -900,6 +900,7 @@
         "add": "Aggiunta",
         "update": "Aggiornamento",
         "login_failed": "Accesso fallito",
+        "login_ok": "Accesso riuscito",
         "login_missing_user": "Accesso con utente inesistente",
         "no_login_tried": "Nessun accesso tentato",
         "algo_negotiation_failed": "Negoziazione algoritmo fallita",

+ 9 - 2
templates/webadmin/events.html

@@ -385,6 +385,7 @@ explicit grant from the SFTPGo Team ([email protected]).
     function selectLogEvents(){
         let idActions = $('#idActions');
         idActions.empty();
+        idActions.append(new Option($.t('events.login_ok'),"5",false,false));
         idActions.append(new Option($.t('events.login_failed'),"1",false,false));
         idActions.append(new Option($.t('events.login_missing_user'),"2",false,false));
         idActions.append(new Option($.t('events.no_login_tried'),"3",false,false));
@@ -875,6 +876,8 @@ explicit grant from the SFTPGo Team ([email protected]).
                                         return  $.t('events.no_login_tried');
                                     case 4:
                                         return  $.t('events.algo_negotiation_failed');
+                                    case 5:
+                                        return  $.t('events.login_ok');
                                     default:
                                         console.log(`unknown log action "${data}"`);
                                         return "";
@@ -914,7 +917,9 @@ explicit grant from the SFTPGo Team ([email protected]).
                         defaultContent: "",
                         render: function(data, type, row) {
                             if (type === 'display') {
-                                return escapeHTML(data);
+                                if (data){
+                                    return escapeHTML(data);
+                                }
                             }
                             return data;
                         }
@@ -924,7 +929,9 @@ explicit grant from the SFTPGo Team ([email protected]).
                         defaultContent: "",
                         render: function(data, type, row) {
                             if (type === 'display') {
-                                return escapeHTML(data);
+                                if (data){
+                                    return escapeHTML(data);
+                                }
                             }
                             return "";
                         }