Explorar el Código

command hooks: allow to pass custom arguments

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino hace 2 años
padre
commit
7349598b19

+ 3 - 1
docs/full-configuration.md

@@ -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.

+ 1 - 1
go.mod

@@ -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
 )
 )

+ 2 - 2
go.sum

@@ -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-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 h1:yuLAip3bfURHClMG9VBdzPrQvCWjWiWUTBGV+/fCbUs=
+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=

+ 40 - 7
internal/command/command.go

@@ -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
-			env = append(env, cmd.Env...)
-			break
+			if cmd.Hook == "" || cmd.Hook == hook {
+				timeout = time.Duration(cmd.Timeout) * time.Second
+				env = append(env, cmd.Env...)
+				args = cmd.Args
+				break
+			}
 		}
 		}
 	}
 	}
 
 
-	return timeout, env
+	return timeout, env, args
 }
 }

+ 65 - 3
internal/command/command_test.go

@@ -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")
+	}
 }
 }

+ 2 - 2
internal/common/actions.go

@@ -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()

+ 6 - 6
internal/common/common.go

@@ -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))

+ 2 - 2
internal/common/dataretention.go

@@ -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()

+ 2 - 2
internal/dataprovider/actions.go

@@ -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),

+ 10 - 10
internal/dataprovider/dataprovider.go

@@ -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)),