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
10 changed files with 133 additions and 36 deletions

View File

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

View File

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