mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
command hooks: allow to pass custom arguments
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
7f19f9f39c
commit
7349598b19
10 changed files with 133 additions and 36 deletions
|
@ -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:
|
||||
- `path`, string. Define the command path as defined in the hook configuration
|
||||
- `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)
|
||||
- `secrets`
|
||||
- `url`, string. Defines the URI to the KMS service. Default: blank.
|
||||
|
|
2
go.mod
2
go.mod
|
@ -69,7 +69,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
|
||||
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
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
|
4
go.sum
4
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-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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -28,8 +30,25 @@ const (
|
|||
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 (
|
||||
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
|
||||
|
@ -41,10 +60,14 @@ type Command struct {
|
|||
// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env
|
||||
// vars that SFTPGo sets
|
||||
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".
|
||||
// These values are added to the global environment variables if any
|
||||
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
|
||||
|
@ -93,23 +116,33 @@ func (c Config) Initialize() error {
|
|||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
var args []string
|
||||
timeout := time.Duration(config.Timeout) * time.Second
|
||||
env = append(env, config.Env...)
|
||||
for _, cmd := range config.Commands {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -33,15 +33,17 @@ func TestCommandConfig(t *testing.T) {
|
|||
assert.Equal(t, cfg.Timeout, config.Timeout)
|
||||
assert.Equal(t, cfg.Env, config.Env)
|
||||
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.Contains(t, env, "a=b")
|
||||
assert.Len(t, args, 0)
|
||||
|
||||
cfg.Commands = []Command{
|
||||
{
|
||||
Path: "cmd1",
|
||||
Timeout: 30,
|
||||
Env: []string{"c=d"},
|
||||
Args: []string{"1", "", "2"},
|
||||
},
|
||||
{
|
||||
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].Timeout, config.Commands[0].Timeout)
|
||||
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.Timeout, config.Commands[1].Timeout)
|
||||
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.Contains(t, env, "a=b")
|
||||
assert.Contains(t, env, "c=d")
|
||||
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.Contains(t, env, "a=b")
|
||||
assert.NotContains(t, env, "c=d")
|
||||
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) {
|
||||
|
@ -116,4 +166,16 @@ func TestConfigErrors(t *testing.T) {
|
|||
if assert.Error(t, err) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,11 +272,11 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
|
|||
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)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, Config.Actions.Hook)
|
||||
cmd := exec.CommandContext(ctx, Config.Actions.Hook, args...)
|
||||
cmd.Env = append(env, notificationAsEnvVars(event)...)
|
||||
|
||||
startTime := time.Now()
|
||||
|
|
|
@ -578,11 +578,11 @@ func (c *Configuration) ExecuteStartupHook() error {
|
|||
return err
|
||||
}
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.StartupHook)
|
||||
cmd := exec.CommandContext(ctx, c.StartupHook, args...)
|
||||
cmd.Env = env
|
||||
err := cmd.Run()
|
||||
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)
|
||||
return
|
||||
}
|
||||
timeout, env := command.GetConfig(c.PostDisconnectHook)
|
||||
timeout, env, args := command.GetConfig(c.PostDisconnectHook, command.HookPostDisconnect)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
cmd := exec.CommandContext(ctx, c.PostDisconnectHook)
|
||||
cmd := exec.CommandContext(ctx, c.PostDisconnectHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
|
||||
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)
|
||||
return err
|
||||
}
|
||||
timeout, env := command.GetConfig(c.PostConnectHook)
|
||||
timeout, env, args := command.GetConfig(c.PostConnectHook, command.HookPostConnect)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.PostConnectHook)
|
||||
cmd := exec.CommandContext(ctx, c.PostConnectHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
|
||||
|
|
|
@ -448,11 +448,11 @@ func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck er
|
|||
c.conn.Log(logger.LevelError, "%v", 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)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, Config.DataRetentionHook)
|
||||
cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%v", string(jsonData)))
|
||||
err := cmd.Run()
|
||||
|
|
|
@ -128,11 +128,11 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName
|
|||
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)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.Actions.Hook)
|
||||
cmd := exec.CommandContext(ctx, config.Actions.Hook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),
|
||||
|
|
|
@ -3329,11 +3329,11 @@ func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge,
|
|||
|
||||
func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
||||
authResult := 0
|
||||
timeout, env := command.GetConfig(authHook)
|
||||
timeout, env, args := command.GetConfig(authHook, command.HookKeyboardInteractive)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, authHook)
|
||||
cmd := exec.CommandContext(ctx, authHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username),
|
||||
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))
|
||||
}
|
||||
timeout, env := command.GetConfig(config.CheckPasswordHook)
|
||||
timeout, env, args := command.GetConfig(config.CheckPasswordHook, command.HookCheckPassword)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.CheckPasswordHook)
|
||||
cmd := exec.CommandContext(ctx, config.CheckPasswordHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
||||
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))
|
||||
}
|
||||
timeout, env := command.GetConfig(config.PreLoginHook)
|
||||
timeout, env, args := command.GetConfig(config.PreLoginHook, command.HookPreLogin)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.PreLoginHook)
|
||||
cmd := exec.CommandContext(ctx, config.PreLoginHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
||||
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)
|
||||
return
|
||||
}
|
||||
timeout, env := command.GetConfig(config.PostLoginHook)
|
||||
timeout, env, args := command.GetConfig(config.PostLoginHook, command.HookPostLogin)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.PostLoginHook)
|
||||
cmd := exec.CommandContext(ctx, config.PostLoginHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
||||
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)
|
||||
}
|
||||
}
|
||||
timeout, env := command.GetConfig(config.ExternalAuthHook)
|
||||
timeout, env, args := command.GetConfig(config.ExternalAuthHook, command.HookExternalAuth)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.ExternalAuthHook)
|
||||
cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)),
|
||||
|
|
Loading…
Reference in a new issue