command hooks: allow to pass custom arguments

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-09-20 13:58:44 +02:00
parent 7f19f9f39c
commit 7349598b19
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
10 changed files with 133 additions and 36 deletions

View file

@ -367,7 +367,9 @@ The configuration file contains the following sections:
- `commands`, list of structs. Allow to customize configuration per-command. Each struct has the following fields: - `commands`, list of structs. Allow to customize configuration per-command. Each struct has the following fields:
- `path`, string. Define the command path as defined in the hook configuration - `path`, string. Define the command path as defined in the hook configuration
- `timeout`, integer. This value overrides the global timeout if set - `timeout`, integer. This value overrides the global timeout if set
- `env`, list of strings. These values are added to the environment variables defined for all commands, if any - `env`, list of strings. These values are added to the environment variables defined for all commands, if any. Default: empty
- `args`, list of strings. Arguments to pass to the command identified by `path`. Default: empty
- `hook`, string. If not empty this configuration only apply to the specified hook name. Supported hook names: `fs_actions`, `provider_actions`, `startup`, `post_connect`, `post_disconnect`, `data_retention`, `check_password`, `pre_login`, `post_login`, `external_auth`, `keyboard_interactive`. Default: empty
- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md) - **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md)
- `secrets` - `secrets`
- `url`, string. Defines the URI to the KMS service. Default: blank. - `url`, string. Defines the URI to the KMS service. Default: blank.

2
go.mod
View file

@ -69,7 +69,7 @@ require (
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45
google.golang.org/api v0.96.0 google.golang.org/api v0.96.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )

4
go.sum
View file

@ -1000,8 +1000,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 h1:yuLAip3bfURHClMG9VBdzPrQvCWjWiWUTBGV+/fCbUs=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View file

@ -20,6 +20,8 @@ import (
"os" "os"
"strings" "strings"
"time" "time"
"github.com/drakkan/sftpgo/v2/internal/util"
) )
const ( const (
@ -28,8 +30,25 @@ const (
defaultTimeout = 30 defaultTimeout = 30
) )
// Supported hook names
const (
HookFsActions = "fs_actions"
HookProviderActions = "provider_actions"
HookStartup = "startup"
HookPostConnect = "post_connect"
HookPostDisconnect = "post_disconnect"
HookDataRetention = "data_retention"
HookCheckPassword = "check_password"
HookPreLogin = "pre_login"
HookPostLogin = "post_login"
HookExternalAuth = "external_auth"
HookKeyboardInteractive = "keyboard_interactive"
)
var ( var (
config Config config Config
supportedHooks = []string{HookFsActions, HookProviderActions, HookStartup, HookPostConnect, HookPostDisconnect,
HookDataRetention, HookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}
) )
// Command define the configuration for a specific commands // Command define the configuration for a specific commands
@ -41,10 +60,14 @@ type Command struct {
// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env // Do not use variables with the SFTPGO_ prefix to avoid conflicts with env
// vars that SFTPGo sets // vars that SFTPGo sets
Timeout int `json:"timeout" mapstructure:"timeout"` Timeout int `json:"timeout" mapstructure:"timeout"`
// Env defines additional environment variable for the commands. // Env defines additional environment variable for the command.
// Each entry is of the form "key=value". // Each entry is of the form "key=value".
// These values are added to the global environment variables if any // These values are added to the global environment variables if any
Env []string `json:"env" mapstructure:"env"` Env []string `json:"env" mapstructure:"env"`
// Args defines arguments to pass to the specified command
Args []string `json:"args" mapstructure:"args"`
// if not empty both command path and hook name must match
Hook string `json:"hook" mapstructure:"hook"`
} }
// Config defines the configuration for external commands such as // Config defines the configuration for external commands such as
@ -93,23 +116,33 @@ func (c Config) Initialize() error {
return fmt.Errorf("invalid env var %#v for command %#v", env, cmd.Path) return fmt.Errorf("invalid env var %#v for command %#v", env, cmd.Path)
} }
} }
// don't validate args, we allow to pass empty arguments
if cmd.Hook != "" {
if !util.Contains(supportedHooks, cmd.Hook) {
return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
}
}
} }
config = c config = c
return nil return nil
} }
// GetConfig returns the configuration for the specified command // GetConfig returns the configuration for the specified command
func GetConfig(command string) (time.Duration, []string) { func GetConfig(command, hook string) (time.Duration, []string, []string) {
env := os.Environ() env := os.Environ()
var args []string
timeout := time.Duration(config.Timeout) * time.Second timeout := time.Duration(config.Timeout) * time.Second
env = append(env, config.Env...) env = append(env, config.Env...)
for _, cmd := range config.Commands { for _, cmd := range config.Commands {
if cmd.Path == command { if cmd.Path == command {
timeout = time.Duration(cmd.Timeout) * time.Second if cmd.Hook == "" || cmd.Hook == hook {
env = append(env, cmd.Env...) timeout = time.Duration(cmd.Timeout) * time.Second
break env = append(env, cmd.Env...)
args = cmd.Args
break
}
} }
} }
return timeout, env return timeout, env, args
} }

View file

@ -33,15 +33,17 @@ func TestCommandConfig(t *testing.T) {
assert.Equal(t, cfg.Timeout, config.Timeout) assert.Equal(t, cfg.Timeout, config.Timeout)
assert.Equal(t, cfg.Env, config.Env) assert.Equal(t, cfg.Env, config.Env)
assert.Len(t, cfg.Commands, 0) assert.Len(t, cfg.Commands, 0)
timeout, env := GetConfig("cmd") timeout, env, args := GetConfig("cmd", "")
assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout) assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b") assert.Contains(t, env, "a=b")
assert.Len(t, args, 0)
cfg.Commands = []Command{ cfg.Commands = []Command{
{ {
Path: "cmd1", Path: "cmd1",
Timeout: 30, Timeout: 30,
Env: []string{"c=d"}, Env: []string{"c=d"},
Args: []string{"1", "", "2"},
}, },
{ {
Path: "cmd2", Path: "cmd2",
@ -57,20 +59,68 @@ func TestCommandConfig(t *testing.T) {
assert.Equal(t, cfg.Commands[0].Path, config.Commands[0].Path) assert.Equal(t, cfg.Commands[0].Path, config.Commands[0].Path)
assert.Equal(t, cfg.Commands[0].Timeout, config.Commands[0].Timeout) assert.Equal(t, cfg.Commands[0].Timeout, config.Commands[0].Timeout)
assert.Equal(t, cfg.Commands[0].Env, config.Commands[0].Env) assert.Equal(t, cfg.Commands[0].Env, config.Commands[0].Env)
assert.Equal(t, cfg.Commands[0].Args, config.Commands[0].Args)
assert.Equal(t, cfg.Commands[1].Path, config.Commands[1].Path) assert.Equal(t, cfg.Commands[1].Path, config.Commands[1].Path)
assert.Equal(t, cfg.Timeout, config.Commands[1].Timeout) assert.Equal(t, cfg.Timeout, config.Commands[1].Timeout)
assert.Equal(t, cfg.Commands[1].Env, config.Commands[1].Env) assert.Equal(t, cfg.Commands[1].Env, config.Commands[1].Env)
assert.Equal(t, cfg.Commands[1].Args, config.Commands[1].Args)
} }
timeout, env = GetConfig("cmd1") timeout, env, args = GetConfig("cmd1", "")
assert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout) assert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b") assert.Contains(t, env, "a=b")
assert.Contains(t, env, "c=d") assert.Contains(t, env, "c=d")
assert.NotContains(t, env, "e=f") assert.NotContains(t, env, "e=f")
timeout, env = GetConfig("cmd2") if assert.Len(t, args, 3) {
assert.Equal(t, "1", args[0])
assert.Empty(t, args[1])
assert.Equal(t, "2", args[2])
}
timeout, env, args = GetConfig("cmd2", "")
assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout) assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b") assert.Contains(t, env, "a=b")
assert.NotContains(t, env, "c=d") assert.NotContains(t, env, "c=d")
assert.Contains(t, env, "e=f") assert.Contains(t, env, "e=f")
assert.Len(t, args, 0)
cfg.Commands = []Command{
{
Path: "cmd1",
Timeout: 30,
Env: []string{"c=d"},
Args: []string{"1", "", "2"},
Hook: HookCheckPassword,
},
{
Path: "cmd1",
Timeout: 0,
Env: []string{"e=f"},
Hook: HookExternalAuth,
},
}
err = cfg.Initialize()
require.NoError(t, err)
timeout, env, args = GetConfig("cmd1", "")
assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b")
assert.NotContains(t, env, "c=d")
assert.NotContains(t, env, "e=f")
assert.Len(t, args, 0)
timeout, env, args = GetConfig("cmd1", HookCheckPassword)
assert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b")
assert.Contains(t, env, "c=d")
assert.NotContains(t, env, "e=f")
if assert.Len(t, args, 3) {
assert.Equal(t, "1", args[0])
assert.Empty(t, args[1])
assert.Equal(t, "2", args[2])
}
timeout, env, args = GetConfig("cmd1", HookExternalAuth)
assert.Equal(t, time.Duration(cfg.Timeout)*time.Second, timeout)
assert.Contains(t, env, "a=b")
assert.NotContains(t, env, "c=d")
assert.Contains(t, env, "e=f")
assert.Len(t, args, 0)
} }
func TestConfigErrors(t *testing.T) { func TestConfigErrors(t *testing.T) {
@ -116,4 +166,16 @@ func TestConfigErrors(t *testing.T) {
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "invalid env var") assert.Contains(t, err.Error(), "invalid env var")
} }
c.Commands = []Command{
{
Path: "path",
Timeout: 30,
Env: []string{"a=b"},
Hook: "invali",
},
}
err = c.Initialize()
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "invalid hook name")
}
} }

View file

@ -272,11 +272,11 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
return err return err
} }
timeout, env := command.GetConfig(Config.Actions.Hook) timeout, env, args := command.GetConfig(Config.Actions.Hook, command.HookFsActions)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, Config.Actions.Hook) cmd := exec.CommandContext(ctx, Config.Actions.Hook, args...)
cmd.Env = append(env, notificationAsEnvVars(event)...) cmd.Env = append(env, notificationAsEnvVars(event)...)
startTime := time.Now() startTime := time.Now()

View file

@ -578,11 +578,11 @@ func (c *Configuration) ExecuteStartupHook() error {
return err return err
} }
startTime := time.Now() startTime := time.Now()
timeout, env := command.GetConfig(c.StartupHook) timeout, env, args := command.GetConfig(c.StartupHook, command.HookStartup)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, c.StartupHook) 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: %v, error: %v", time.Since(startTime), err)
@ -624,12 +624,12 @@ func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username
logger.Debug(protocol, connID, "invalid post disconnect hook %#v", c.PostDisconnectHook) logger.Debug(protocol, connID, "invalid post disconnect hook %#v", c.PostDisconnectHook)
return return
} }
timeout, env := command.GetConfig(c.PostDisconnectHook) timeout, env, args := command.GetConfig(c.PostDisconnectHook, command.HookPostDisconnect)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
startTime := time.Now() startTime := time.Now()
cmd := exec.CommandContext(ctx, c.PostDisconnectHook) 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=%v", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%v", username), fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%v", username),
@ -684,11 +684,11 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
logger.Warn(protocol, "", "Login from ip %#v denied: %v", ipAddr, err) logger.Warn(protocol, "", "Login from ip %#v denied: %v", ipAddr, err)
return err return err
} }
timeout, env := command.GetConfig(c.PostConnectHook) timeout, env, args := command.GetConfig(c.PostConnectHook, command.HookPostConnect)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, c.PostConnectHook) 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=%v", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol)) fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))

View file

@ -448,11 +448,11 @@ func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck er
c.conn.Log(logger.LevelError, "%v", err) c.conn.Log(logger.LevelError, "%v", err)
return err return err
} }
timeout, env := command.GetConfig(Config.DataRetentionHook) timeout, env, args := command.GetConfig(Config.DataRetentionHook, command.HookDataRetention)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, Config.DataRetentionHook) 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=%v", string(jsonData)))
err := cmd.Run() err := cmd.Run()

View file

@ -128,11 +128,11 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName
return err return err
} }
timeout, env := command.GetConfig(config.Actions.Hook) timeout, env, args := command.GetConfig(config.Actions.Hook, command.HookProviderActions)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.Actions.Hook) cmd := exec.CommandContext(ctx, config.Actions.Hook, args...)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation), fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType), fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),

View file

@ -3329,11 +3329,11 @@ func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge,
func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) { func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
authResult := 0 authResult := 0
timeout, env := command.GetConfig(authHook) timeout, env, args := command.GetConfig(authHook, command.HookKeyboardInteractive)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, authHook) 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=%v", user.Username),
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
@ -3462,11 +3462,11 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
} }
return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))
} }
timeout, env := command.GetConfig(config.CheckPasswordHook) timeout, env, args := command.GetConfig(config.CheckPasswordHook, command.HookCheckPassword)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.CheckPasswordHook) 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=%v", username),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password),
@ -3523,11 +3523,11 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
} }
return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))
} }
timeout, env := command.GetConfig(config.PreLoginHook) timeout, env, args := command.GetConfig(config.PreLoginHook, command.HookPreLogin)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.PreLoginHook) 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=%v", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod), fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
@ -3667,11 +3667,11 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
user.Username, ip, protocol, respCode, time.Since(startTime), err) user.Username, ip, protocol, respCode, time.Since(startTime), err)
return return
} }
timeout, env := command.GetConfig(config.PostLoginHook) timeout, env, args := command.GetConfig(config.PostLoginHook, command.HookPostLogin)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.PostLoginHook) 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=%v", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip), fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
@ -3735,11 +3735,11 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
return nil, fmt.Errorf("unable to serialize user as JSON: %w", err) return nil, fmt.Errorf("unable to serialize user as JSON: %w", err)
} }
} }
timeout, env := command.GetConfig(config.ExternalAuthHook) timeout, env, args := command.GetConfig(config.ExternalAuthHook, command.HookExternalAuth)
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.ExternalAuthHook) 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=%v", username),
fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)), fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)),