also capture logs for pre-login and check-password commands

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2023-02-26 15:15:34 +01:00
parent ec67b67e9e
commit 874776bd12
10 changed files with 143 additions and 130 deletions

View File

@@ -22,6 +22,8 @@ Global environment variables are cleared, for security reasons, when the script
The program must write, on its standard output, the expected JSON serialized response described above. The program must write, on its standard output, the expected JSON serialized response described above.
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `check_password_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields: If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `username` - `username`

View File

@@ -15,6 +15,8 @@ The program must write, on its standard output:
- an empty string (or no response at all) if the user should not be created/updated - an empty string (or no response at all) if the user should not be created/updated
- or the SFTPGo user, JSON serialized, if you want to create or update the given user - or the SFTPGo user, JSON serialized, if you want to create or update the given user
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `pre_login_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`. If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`.
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON. The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.

View File

@@ -21,7 +21,7 @@ The program must write, on its standard output:
- an empty string, or no response at all, if authentication succeeds and the existing SFTPGo user does not need to be updated. This means that the credentials already stored in SFTPGo must match those used for the current authentication. - an empty string, or no response at all, if authentication succeeds and the existing SFTPGo user does not need to be updated. This means that the credentials already stored in SFTPGo must match those used for the current authentication.
- a user with an empty username if the authentication fails - a user with an empty username if the authentication fails
Any output of the program on its standard error will be recorded in the sftpgo logs with sender `external_auth_hook`. Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `external_auth_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields: If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:

4
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.21.0 github.com/fclairamb/ftpserverlib v0.21.0
github.com/fclairamb/go-log v0.4.1 github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.10.0 github.com/go-acme/lego/v4 v4.10.2
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/jwtauth/v5 v5.1.0 github.com/go-chi/jwtauth/v5 v5.1.0
github.com/go-chi/render v1.0.2 github.com/go-chi/render v1.0.2
@@ -133,7 +133,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect github.com/miekg/dns v1.1.51 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect

9
go.sum
View File

@@ -920,8 +920,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-acme/lego/v4 v4.10.0 h1:G4Cgq4lsPxCjqsTKsqhUjRs3oKAGVMFPhvrl6kzzs44= github.com/go-acme/lego/v4 v4.10.2 h1:5eW3qmda5v/LP21v1Hj70edKY1jeFZQwO617tdkwp6Q=
github.com/go-acme/lego/v4 v4.10.0/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U= github.com/go-acme/lego/v4 v4.10.2/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA= github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
@@ -1516,8 +1516,9 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
@@ -2098,6 +2099,7 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2513,6 +2515,7 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -307,7 +307,7 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
startTime := time.Now() startTime := time.Now()
err := cmd.Run() err := cmd.Run()
logger.Debug(event.Protocol, "", "executed command %#v, elapsed: %v, error: %v", logger.Debug(event.Protocol, "", "executed command %q, elapsed: %s, error: %v",
Config.Actions.Hook, time.Since(startTime), err) Config.Actions.Hook, time.Since(startTime), err)
return err return err

View File

@@ -662,7 +662,7 @@ func (c *Configuration) ExecuteStartupHook() error {
cmd := exec.CommandContext(ctx, c.StartupHook, args...) cmd := exec.CommandContext(ctx, c.StartupHook, args...)
cmd.Env = env cmd.Env = env
err := cmd.Run() err := cmd.Run()
logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, error: %v", time.Since(startTime), err) logger.Debug(logSender, "", "Startup hook executed, elapsed: %s, error: %v", time.Since(startTime), err)
return nil return nil
} }
@@ -708,12 +708,12 @@ func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username
startTime := time.Now() startTime := time.Now()
cmd := exec.CommandContext(ctx, c.PostDisconnectHook, args...) cmd := exec.CommandContext(ctx, c.PostDisconnectHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr), fmt.Sprintf("SFTPGO_CONNECTION_IP=%s", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%v", username), fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_CONNECTION_DURATION=%v", connDuration), fmt.Sprintf("SFTPGO_CONNECTION_DURATION=%d", connDuration),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol)) fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%s", protocol))
err := cmd.Run() err := cmd.Run()
logger.Debug(protocol, connID, "Post disconnect hook executed, elapsed: %v error: %v", time.Since(startTime), err) logger.Debug(protocol, connID, "Post disconnect hook executed, elapsed: %s error: %v", time.Since(startTime), err)
} }
func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) { func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {
@@ -767,11 +767,11 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
cmd := exec.CommandContext(ctx, c.PostConnectHook, args...) cmd := exec.CommandContext(ctx, c.PostConnectHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr), fmt.Sprintf("SFTPGO_CONNECTION_IP=%s", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol)) fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%s", protocol))
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
logger.Warn(protocol, "", "Login from ip %#v denied, connect hook error: %v", ipAddr, err) logger.Warn(protocol, "", "Login from ip %q denied, connect hook error: %v", ipAddr, err)
} }
return err return err
} }

View File

@@ -460,10 +460,10 @@ func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck er
cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...) cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%v", string(jsonData))) fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", string(jsonData)))
err := cmd.Run() err := cmd.Run()
c.conn.Log(logger.LevelDebug, "notified result using command: %v, elapsed: %v err: %v", c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
Config.DataRetentionHook, time.Since(startTime), err) Config.DataRetentionHook, time.Since(startTime), err)
return err return err
} }

View File

@@ -152,7 +152,7 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName,
startTime := time.Now() startTime := time.Now()
err := cmd.Run() err := cmd.Run()
providerLog(logger.LevelDebug, "executed command %#v, elapsed: %v, error: %v", config.Actions.Hook, providerLog(logger.LevelDebug, "executed command %q, elapsed: %s, error: %v", config.Actions.Hook,
time.Since(startTime), err) time.Since(startTime), err)
return err return err
} }

View File

@@ -857,7 +857,7 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
cnf.BackupsPath = getConfigPath(cnf.BackupsPath, basePath) cnf.BackupsPath = getConfigPath(cnf.BackupsPath, basePath)
if cnf.BackupsPath == "" { if cnf.BackupsPath == "" {
return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath) return fmt.Errorf("required directory is invalid, backup path %q", cnf.BackupsPath)
} }
absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath) absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)
if err != nil { if err != nil {
@@ -936,7 +936,7 @@ func validateHooks() error {
for _, hook := range hooks { for _, hook := range hooks {
if !filepath.IsAbs(hook) { if !filepath.IsAbs(hook) {
return fmt.Errorf("invalid hook: %#v must be an absolute path", hook) return fmt.Errorf("invalid hook: %q must be an absolute path", hook)
} }
_, err := os.Stat(hook) _, err := os.Stat(hook)
if err != nil { if err != nil {
@@ -1100,7 +1100,7 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco
} }
if loginMethod == LoginMethodTLSCertificate { if loginMethod == LoginMethodTLSCertificate {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) { if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return fmt.Errorf("certificate login method is not allowed for user %#v", user.User.Username) return fmt.Errorf("certificate login method is not allowed for user %q", user.User.Username)
} }
return nil return nil
} }
@@ -1143,7 +1143,7 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
return user, loginMethod, err return user, loginMethod, err
} }
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) { if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username) return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %q", user.Username)
} }
if loginMethod == LoginMethodTLSCertificateAndPwd { if loginMethod == LoginMethodTLSCertificateAndPwd {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) { if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
@@ -1577,7 +1577,7 @@ func DeleteShare(shareID string, executor, ipAddress, role string) error {
// ShareExists returns the share with the given ID if it exists // ShareExists returns the share with the given ID if it exists
func ShareExists(shareID, username string) (Share, error) { func ShareExists(shareID, username string) (Share, error) {
if shareID == "" { if shareID == "" {
return Share{}, util.NewRecordNotFoundError(fmt.Sprintf("Share %#v does not exist", shareID)) return Share{}, util.NewRecordNotFoundError(fmt.Sprintf("Share %q does not exist", shareID))
} }
return provider.shareExists(shareID, username) return provider.shareExists(shareID, username)
} }
@@ -1780,7 +1780,7 @@ func DeleteAPIKey(keyID string, executor, ipAddress, role string) error {
// APIKeyExists returns the API key with the given ID if it exists // APIKeyExists returns the API key with the given ID if it exists
func APIKeyExists(keyID string) (APIKey, error) { func APIKeyExists(keyID string) (APIKey, error) {
if keyID == "" { if keyID == "" {
return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %#v does not exist", keyID)) return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %q does not exist", keyID))
} }
return provider.apiKeyExists(keyID) return provider.apiKeyExists(keyID)
} }
@@ -2126,7 +2126,7 @@ func GetActiveTransfers(from time.Time) ([]ActiveTransfer, error) {
func AddSharedSession(session Session) error { func AddSharedSession(session Session) error {
err := provider.addSharedSession(session) err := provider.addSharedSession(session)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to add shared session, key %#v, type: %v, err: %v", providerLog(logger.LevelError, "unable to add shared session, key %q, type: %v, err: %v",
session.Key, session.Type, err) session.Key, session.Type, err)
} }
return err return err
@@ -2136,7 +2136,7 @@ func AddSharedSession(session Session) error {
func DeleteSharedSession(key string) error { func DeleteSharedSession(key string) error {
err := provider.deleteSharedSession(key) err := provider.deleteSharedSession(key)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to add shared session, key %#v, err: %v", key, err) providerLog(logger.LevelError, "unable to add shared session, key %q, err: %v", key, err)
} }
return err return err
} }
@@ -2487,10 +2487,10 @@ func buildUserHomeDir(user *User) {
func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
if folder.QuotaSize < -1 { if folder.QuotaSize < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath))
} }
if folder.QuotaFiles < -1 { if folder.QuotaFiles < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath))
} }
if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) {
return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v",
@@ -2537,7 +2537,7 @@ func validateUserGroups(user *User) error {
hasPrimary = true hasPrimary = true
} }
if groupNames[g.Name] { if groupNames[g.Name] {
return util.NewValidationError(fmt.Sprintf("the group %#v is duplicated", g.Name)) return util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name))
} }
groupNames[g.Name] = true groupNames[g.Name] = true
} }
@@ -2594,7 +2594,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
return util.NewValidationError("totp: config name is mandatory") return util.NewValidationError("totp: config name is mandatory")
} }
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) { if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
return util.NewValidationError(fmt.Sprintf("totp: config name %#v not found", c.ConfigName)) return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
} }
if c.Secret.IsEmpty() { if c.Secret.IsEmpty() {
return util.NewValidationError("totp: secret is mandatory") return util.NewValidationError("totp: secret is mandatory")
@@ -2610,7 +2610,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
} }
for _, protocol := range c.Protocols { for _, protocol := range c.Protocols {
if !util.Contains(MFAProtocols, protocol) { if !util.Contains(MFAProtocols, protocol) {
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %#v", protocol)) return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
} }
} }
return nil return nil
@@ -2636,14 +2636,14 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
permissions := make(map[string][]string) permissions := make(map[string][]string)
for dir, perms := range permsToCheck { for dir, perms := range permsToCheck {
if len(perms) == 0 && dir == "/" { if len(perms) == 0 && dir == "/" {
return permissions, util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir)) return permissions, util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %q", dir))
} }
if len(perms) > len(ValidPerms) { if len(perms) > len(ValidPerms) {
return permissions, util.NewValidationError("invalid permissions") return permissions, util.NewValidationError("invalid permissions")
} }
for _, p := range perms { for _, p := range perms {
if !util.Contains(ValidPerms, p) { if !util.Contains(ValidPerms, p) {
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %#v", p)) return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
} }
} }
cleanedDir := filepath.ToSlash(path.Clean(dir)) cleanedDir := filepath.ToSlash(path.Clean(dir))
@@ -2651,10 +2651,10 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
cleanedDir = strings.TrimSuffix(cleanedDir, "/") cleanedDir = strings.TrimSuffix(cleanedDir, "/")
} }
if !path.IsAbs(cleanedDir) { if !path.IsAbs(cleanedDir) {
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir)) return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %q", dir))
} }
if dir != cleanedDir && cleanedDir == "/" { if dir != cleanedDir && cleanedDir == "/" {
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir)) return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
} }
if util.Contains(perms, PermAny) { if util.Contains(perms, PermAny) {
permissions[cleanedDir] = []string{PermAny} permissions[cleanedDir] = []string{PermAny}
@@ -2710,16 +2710,16 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, f := range baseFilters.FilePatterns { for _, f := range baseFilters.FilePatterns {
cleanedPath := filepath.ToSlash(path.Clean(f.Path)) cleanedPath := filepath.ToSlash(path.Clean(f.Path))
if !path.IsAbs(cleanedPath) { if !path.IsAbs(cleanedPath) {
return util.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path)) return util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path))
} }
if util.Contains(filteredPaths, cleanedPath) { if util.Contains(filteredPaths, cleanedPath) {
return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path)) return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path))
} }
if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 {
return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path)) return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %q", f.Path))
} }
if f.DenyPolicy < sdk.DenyPolicyDefault || f.DenyPolicy > sdk.DenyPolicyHide { if f.DenyPolicy < sdk.DenyPolicyDefault || f.DenyPolicy > sdk.DenyPolicyHide {
return util.NewValidationError(fmt.Sprintf("invalid deny policy %v for path %#v", f.DenyPolicy, f.Path)) return util.NewValidationError(fmt.Sprintf("invalid deny policy %v for path %q", f.DenyPolicy, f.Path))
} }
f.Path = cleanedPath f.Path = cleanedPath
allowed := make([]string, 0, len(f.AllowedPatterns)) allowed := make([]string, 0, len(f.AllowedPatterns))
@@ -2727,14 +2727,14 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, pattern := range f.AllowedPatterns { for _, pattern := range f.AllowedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern))
} }
allowed = append(allowed, strings.ToLower(pattern)) allowed = append(allowed, strings.ToLower(pattern))
} }
for _, pattern := range f.DeniedPatterns { for _, pattern := range f.DeniedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern))
} }
denied = append(denied, strings.ToLower(pattern)) denied = append(denied, strings.ToLower(pattern))
} }
@@ -2767,14 +2767,14 @@ func validateIPFilters(filters *sdk.BaseUserFilters) error {
for _, IPMask := range filters.DeniedIP { for _, IPMask := range filters.DeniedIP {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v: %v", IPMask, err)) return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %q: %v", IPMask, err))
} }
} }
filters.AllowedIP = util.RemoveDuplicates(filters.AllowedIP, false) filters.AllowedIP = util.RemoveDuplicates(filters.AllowedIP, false)
for _, IPMask := range filters.AllowedIP { for _, IPMask := range filters.AllowedIP {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v: %v", IPMask, err)) return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %q: %v", IPMask, err))
} }
} }
return nil return nil
@@ -2787,7 +2787,7 @@ func validateBandwidthLimit(bl sdk.BandwidthLimit) error {
for _, source := range bl.Sources { for _, source := range bl.Sources {
_, _, err := net.ParseCIDR(source) _, _, err := net.ParseCIDR(source)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse bandwidth limit source %#v: %v", source, err)) return util.NewValidationError(fmt.Sprintf("could not parse bandwidth limit source %q: %v", source, err))
} }
} }
return nil return nil
@@ -2817,7 +2817,7 @@ func validateTransferLimitsFilter(filters *sdk.BaseUserFilters) error {
for _, source := range limit.Sources { for _, source := range limit.Sources {
_, _, err := net.ParseCIDR(source) _, _, err := net.ParseCIDR(source)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %#v: %v", source, err)) return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %q: %v", source, err))
} }
} }
if limit.TotalDataTransfer > 0 { if limit.TotalDataTransfer > 0 {
@@ -2843,13 +2843,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
} }
for _, p := range filters.DeniedProtocols { for _, p := range filters.DeniedProtocols {
if !util.Contains(ValidProtocols, p) { if !util.Contains(ValidProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %#v", p)) return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
} }
} }
for _, p := range filters.TwoFactorAuthProtocols { for _, p := range filters.TwoFactorAuthProtocols {
if !util.Contains(MFAProtocols, p) { if !util.Contains(MFAProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %#v", p)) return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
} }
} }
return nil return nil
@@ -2871,7 +2871,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
} }
for _, loginMethod := range filters.DeniedLoginMethods { for _, loginMethod := range filters.DeniedLoginMethods {
if !util.Contains(ValidLoginMethods, loginMethod) { if !util.Contains(ValidLoginMethods, loginMethod) {
return util.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod)) return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
} }
} }
if err := validateFilterProtocols(filters); err != nil { if err := validateFilterProtocols(filters); err != nil {
@@ -2879,12 +2879,12 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
} }
if filters.TLSUsername != "" { if filters.TLSUsername != "" {
if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) { if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", filters.TLSUsername)) return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
} }
} }
for _, opts := range filters.WebClient { for _, opts := range filters.WebClient {
if !util.Contains(sdk.WebClientOptions, opts) { if !util.Contains(sdk.WebClientOptions, opts) {
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts)) return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
} }
} }
updateFiltersValues(filters) updateFiltersValues(filters)
@@ -2910,7 +2910,7 @@ func validateBaseParams(user *User) error {
return err return err
} }
if user.Email != "" && !util.IsEmailValid(user.Email) { if user.Email != "" && !util.IsEmailValid(user.Email) {
return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email)) return util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email))
} }
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) { if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
@@ -2993,7 +2993,7 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
folder.MappedPath != "" { folder.MappedPath != "" {
cleanedMPath := filepath.Clean(folder.MappedPath) cleanedMPath := filepath.Clean(folder.MappedPath)
if !filepath.IsAbs(cleanedMPath) { if !filepath.IsAbs(cleanedMPath) {
return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %q", folder.MappedPath))
} }
folder.MappedPath = cleanedMPath folder.MappedPath = cleanedMPath
} }
@@ -3122,7 +3122,7 @@ func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certi
if user.Username == tlsCert.Subject.CommonName { if user.Username == tlsCert.Subject.CommonName {
return *user, nil return *user, nil
} }
return *user, fmt.Errorf("CN %#v does not match username %#v", tlsCert.Subject.CommonName, user.Username) return *user, fmt.Errorf("CN %q does not match username %q", tlsCert.Subject.CommonName, user.Username)
} }
return *user, errors.New("TLS certificate is not valid") return *user, errors.New("TLS certificate is not valid")
default: default:
@@ -3156,7 +3156,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if !user.Filters.Hooks.CheckPasswordDisabled { if !user.Filters.Hooks.CheckPasswordDisabled {
hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol) hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol)
if err != nil { if err != nil {
providerLog(logger.LevelDebug, "error executing check password hook for user %#v, ip %v, protocol %v: %v", providerLog(logger.LevelDebug, "error executing check password hook for user %q, ip %v, protocol %v: %v",
user.Username, ip, protocol, err) user.Username, ip, protocol, err)
return *user, errors.New("unable to check credentials") return *user, errors.New("unable to check credentials")
} }
@@ -3164,15 +3164,15 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
case -1: case -1:
// no hook configured // no hook configured
case 1: case 1:
providerLog(logger.LevelDebug, "password accepted by check password hook for user %#v, ip %v, protocol %v", providerLog(logger.LevelDebug, "password accepted by check password hook for user %q, ip %v, protocol %v",
user.Username, ip, protocol) user.Username, ip, protocol)
return *user, nil return *user, nil
case 2: case 2:
providerLog(logger.LevelDebug, "partial success from check password hook for user %#v, ip %v, protocol %v", providerLog(logger.LevelDebug, "partial success from check password hook for user %q, ip %v, protocol %v",
user.Username, ip, protocol) user.Username, ip, protocol)
password = hookResponse.ToVerify password = hookResponse.ToVerify
default: default:
providerLog(logger.LevelDebug, "password rejected by check password hook for user %#v, ip %v, protocol %v, status: %v", providerLog(logger.LevelDebug, "password rejected by check password hook for user %q, ip %v, protocol %v, status: %v",
user.Username, ip, protocol, hookResponse.Status) user.Username, ip, protocol, hookResponse.Status)
return *user, ErrInvalidCredentials return *user, ErrInvalidCredentials
} }
@@ -3193,13 +3193,13 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
// the TOTP passcode has six digits // the TOTP passcode has six digits
pwdLen := len(password) pwdLen := len(password)
if pwdLen < 7 { if pwdLen < 7 {
providerLog(logger.LevelDebug, "password len %v is too short to contain a passcode, user %#v, protocol %v", providerLog(logger.LevelDebug, "password len %v is too short to contain a passcode, user %q, protocol %v",
pwdLen, user.Username, protocol) pwdLen, user.Username, protocol)
return "", util.NewValidationError("password too short, cannot contain the passcode") return "", util.NewValidationError("password too short, cannot contain the passcode")
} }
err := user.Filters.TOTPConfig.Secret.TryDecrypt() err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v", providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return "", err return "", err
} }
@@ -3208,7 +3208,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode, match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,
user.Filters.TOTPConfig.Secret.GetPayload()) user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil { if !match || err != nil {
providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v", providerLog(logger.LevelWarn, "invalid passcode for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return "", util.NewValidationError("invalid passcode") return "", util.NewValidationError("invalid passcode")
} }
@@ -3384,7 +3384,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
} }
err = user.Filters.TOTPConfig.Secret.TryDecrypt() err = user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v", providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return 0, err return 0, err
} }
@@ -3398,7 +3398,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0], match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],
user.Filters.TOTPConfig.Secret.GetPayload()) user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil { if !match || err != nil {
providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v", providerLog(logger.LevelWarn, "invalid passcode for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return 0, util.NewValidationError("invalid passcode") return 0, util.NewValidationError("invalid passcode")
} }
@@ -3504,26 +3504,26 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
if len(answers) == 1 && response.CheckPwd > 0 { if len(answers) == 1 && response.CheckPwd > 0 {
if response.CheckPwd == 2 { if response.CheckPwd == 2 {
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) { if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %#v", providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
user.Username) user.Username)
return answers, errors.New("TOTP not enabled for SSH protocol") return answers, errors.New("TOTP not enabled for SSH protocol")
} }
err := user.Filters.TOTPConfig.Secret.TryDecrypt() err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v", providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err) user.Username, protocol, err)
return answers, fmt.Errorf("unable to decrypt TOTP secret: %w", err) return answers, fmt.Errorf("unable to decrypt TOTP secret: %w", err)
} }
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0], match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],
user.Filters.TOTPConfig.Secret.GetPayload()) user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil { if !match || err != nil {
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to validate passcode for user %#v, match? %v, err: %v", providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to validate passcode for user %q, match? %v, err: %v",
user.Username, match, err) user.Username, match, err)
return answers, errors.New("unable to validate TOTP passcode") return answers, errors.New("unable to validate TOTP passcode")
} }
} else { } else {
_, err = checkUserAndPass(user, answers[0], ip, protocol) _, err = checkUserAndPass(user, answers[0], ip, protocol)
providerLog(logger.LevelInfo, "interactive auth hook requested password validation for user %#v, validation error: %v", providerLog(logger.LevelInfo, "interactive auth hook requested password validation for user %q, validation error: %v",
user.Username, err) user.Username, err)
if err != nil { if err != nil {
return answers, err return answers, err
@@ -3563,9 +3563,9 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
cmd := exec.CommandContext(ctx, authHook, args...) cmd := exec.CommandContext(ctx, authHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username), fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", user.Username),
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", user.Password)) fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", user.Password))
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return authResult, err return authResult, err
@@ -3609,7 +3609,7 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
go func() { go func() {
_, err := cmd.Process.Wait() _, err := cmd.Process.Wait()
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "error waiting for #%v process to exit: %v", authHook, err) providerLog(logger.LevelWarn, "error waiting for %q process to exit: %v", authHook, err)
} }
}() }()
@@ -3696,12 +3696,12 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
cmd := exec.CommandContext(ctx, config.CheckPasswordHook, args...) cmd := exec.CommandContext(ctx, config.CheckPasswordHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol), fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
) )
return cmd.Output() return getCmdOutput(cmd, "check_password_hook")
} }
func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) { func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) {
@@ -3728,7 +3728,7 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
var result []byte var result []byte
url, err := url.Parse(config.PreLoginHook) url, err := url.Parse(config.PreLoginHook)
if err != nil { if err != nil {
providerLog(logger.LevelError, "invalid url for pre-login hook %#v, error: %v", config.PreLoginHook, err) providerLog(logger.LevelError, "invalid url for pre-login hook %q, error: %v", config.PreLoginHook, err)
return result, err return result, err
} }
q := url.Query() q := url.Query()
@@ -3757,12 +3757,12 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
cmd := exec.CommandContext(ctx, config.PreLoginHook, args...) cmd := exec.CommandContext(ctx, config.PreLoginHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)), fmt.Sprintf("SFTPGO_LOGIND_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod), fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip), fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol), fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol),
) )
return cmd.Output() return getCmdOutput(cmd, "pre_login_hook")
} }
func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) { func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {
@@ -3776,15 +3776,15 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
startTime := time.Now() startTime := time.Now()
out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON) out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON)
if err != nil { if err != nil {
return u, fmt.Errorf("pre-login hook error: %v, username %#v, ip %v, protocol %v elapsed %v", return u, fmt.Errorf("pre-login hook error: %v, username %q, ip %v, protocol %v elapsed %v",
err, username, ip, protocol, time.Since(startTime)) err, username, ip, protocol, time.Since(startTime))
} }
providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime)) providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %s", time.Since(startTime))
if util.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v", providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %q id: %d",
username, u.ID) username, u.ID)
if u.ID == 0 { if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
return u, nil return u, nil
} }
@@ -3805,7 +3805,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
recoveryCodes := u.Filters.RecoveryCodes recoveryCodes := u.Filters.RecoveryCodes
err = json.Unmarshal(out, &u) err = json.Unmarshal(out, &u)
if err != nil { if err != nil {
return u, fmt.Errorf("invalid pre-login hook response %#v, error: %v", string(out), err) return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", string(out), err)
} }
u.ID = userID u.ID = userID
u.UsedQuotaSize = userUsedQuotaSize u.UsedQuotaSize = userUsedQuotaSize
@@ -3836,7 +3836,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
if err != nil { if err != nil {
return u, err return u, err
} }
providerLog(logger.LevelDebug, "user %#v added/updated from pre-login hook response, id: %v", username, userID) providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, userID)
if userID == 0 { if userID == 0 {
return provider.userExists(username, "") return provider.userExists(username, "")
} }
@@ -3876,7 +3876,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
var url *url.URL var url *url.URL
url, err := url.Parse(config.PostLoginHook) url, err := url.Parse(config.PostLoginHook)
if err != nil { if err != nil {
providerLog(logger.LevelDebug, "Invalid post-login hook %#v", config.PostLoginHook) providerLog(logger.LevelDebug, "Invalid post-login hook %q", config.PostLoginHook)
return return
} }
q := url.Query() q := url.Query()
@@ -3893,7 +3893,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
respCode = resp.StatusCode respCode = resp.StatusCode
resp.Body.Close() resp.Body.Close()
} }
providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, response code: %v, elapsed: %v err: %v", providerLog(logger.LevelDebug, "post login hook executed for user %q, ip %v, protocol %v, response code: %v, elapsed: %v err: %v",
user.Username, ip, protocol, respCode, time.Since(startTime), err) user.Username, ip, protocol, respCode, time.Since(startTime), err)
return return
} }
@@ -3903,14 +3903,14 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
cmd := exec.CommandContext(ctx, config.PostLoginHook, args...) cmd := exec.CommandContext(ctx, config.PostLoginHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)), fmt.Sprintf("SFTPGO_LOGIND_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip), fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod), fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%v", status), fmt.Sprintf("SFTPGO_LOGIND_STATUS=%s", status),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol)) fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol))
startTime := time.Now() startTime := time.Now()
err = cmd.Run() err = cmd.Run()
providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, elapsed %v err: %v", providerLog(logger.LevelDebug, "post login hook executed for user %q, ip %v, protocol %v, elapsed %v err: %v",
user.Username, ip, protocol, time.Since(startTime), err) user.Username, ip, protocol, time.Since(startTime), err)
}() }()
} }
@@ -3971,34 +3971,16 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...) cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)), fmt.Sprintf("SFTPGO_AUTHD_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey), fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%s", pkey),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol), fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%v", strings.ReplaceAll(tlsCert, "\n", "\\n")), fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%s", strings.ReplaceAll(tlsCert, "\n", "\\n")),
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive)) fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
var stdout bytes.Buffer return getCmdOutput(cmd, "external_auth_hook")
cmd.Stdout = &stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
in := bufio.NewScanner(stderr)
for in.Scan() {
logger.Log(logger.LevelWarn, "external_auth_hook", "", "%s", in.Text())
}
return stdout.Bytes(), cmd.Wait()
} }
func updateUserFromExtAuthResponse(user *User, password, pkey string) { func updateUserFromExtAuthResponse(user *User, password, pkey string) {
@@ -4037,14 +4019,14 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
startTime := time.Now() startTime := time.Now()
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, u) out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, u)
if err != nil { if err != nil {
return user, fmt.Errorf("external auth error for user %#v: %v, elapsed: %v", username, err, time.Since(startTime)) return user, fmt.Errorf("external auth error for user %q, elapsed: %s: %w", username, time.Since(startTime), err)
} }
providerLog(logger.LevelDebug, "external auth completed for user %#v, elapsed: %v", username, time.Since(startTime)) providerLog(logger.LevelDebug, "external auth completed for user %q, elapsed: %s", username, time.Since(startTime))
if util.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v", providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %q, id: %d",
username, u.ID) username, u.ID)
if u.ID == 0 { if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
return u, nil return u, nil
} }
@@ -4121,16 +4103,16 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON) out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
if err != nil { if err != nil {
return user, fmt.Errorf("plugin auth error for user %#v: %v, elapsed: %v, auth scope: %v", return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %v",
username, err, time.Since(startTime), authScope) username, err, time.Since(startTime), authScope)
} }
providerLog(logger.LevelDebug, "plugin auth completed for user %#v, elapsed: %v,auth scope: %v", providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v,auth scope: %v",
username, time.Since(startTime), authScope) username, time.Since(startTime), authScope)
if util.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %#v id: %v", providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %v",
username, u.ID) username, u.ID)
if u.ID == 0 { if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
return u, nil return u, nil
} }
@@ -4228,6 +4210,30 @@ func checkReservedUsernames(username string) error {
return nil return nil
} }
func getCmdOutput(cmd *exec.Cmd, sender string) ([]byte, error) {
var stdout bytes.Buffer
cmd.Stdout = &stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
if out := scanner.Text(); out != "" {
logger.Log(logger.LevelWarn, sender, "", out)
}
}
err = cmd.Wait()
return stdout.Bytes(), err
}
func providerLog(level logger.LogLevel, format string, v ...any) { func providerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, logSender, "", format, v...) logger.Log(level, logSender, "", format, v...)
} }