diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 906d64e6..313d55b1 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -477,7 +477,7 @@ The configuration file contains the following sections:
Plugins - **plugins**, list of external plugins. :warning: Please note that the plugin system is experimental, the configuration parameters and interfaces may change in a backward incompatible way in future. Each plugin is configured using a struct with the following fields: - - `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`, `metadata`. + - `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`, `metadata`, `eventsearcher`, `ipfilter`. - `notifier_options`, struct. Defines the options for notifier plugins. - `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin. - `provider_events`, list of strings. Defines the provider events that will be notified to this plugin. @@ -494,6 +494,8 @@ The configuration file contains the following sections: - `args`, list of strings. Optional arguments to pass to the plugin executable. - `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable. - `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client. + - `env_prefix`, string. Defines the prefix for env vars to pass from the SFTPGo process environment to the plugin. Set to `none` to not pass any environment variable, set to `*` to pass all environment variables. If empty, the prefix is returned as the plugin name in uppercase with `-` replaced with `_` and a trailing `_`. For example if the plugin name is `sftpgo-plugin-eventsearch` the prefix will be `SFTPGO_PLUGIN_EVENTSEARCH_` + - `env_vars`, list of strings. Additional environment variable names to pass from the SFTPGo process environment to the plugin.
diff --git a/internal/config/config.go b/internal/config/config.go index c24a6911..1d2aa177 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1046,6 +1046,18 @@ func getPluginsFromEnv(idx int) { isSet = true } + envPrefix, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__ENV_PREFIX", idx)) + if ok { + pluginConfig.EnvPrefix = envPrefix + isSet = true + } + + envVars, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__ENV_VARS", idx)) + if ok { + pluginConfig.EnvVars = envVars + isSet = true + } + if isSet { if len(globalConf.PluginsConfig) > idx { globalConf.PluginsConfig[idx] = pluginConfig diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 82f95894..532cf6bd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -693,6 +693,9 @@ func TestPluginsFromEnv(t *testing.T) { os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeAWS) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusAWS) os.Setenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE", "14") + os.Setenv("SFTPGO_PLUGINS__0__ENV_PREFIX", "prefix_") + os.Setenv("SFTPGO_PLUGINS__0__ENV_VARS", "a, b") + t.Cleanup(func() { os.Unsetenv("SFTPGO_PLUGINS__0__TYPE") os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS") @@ -708,6 +711,8 @@ func TestPluginsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME") os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS") os.Unsetenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE") + os.Unsetenv("SFTPGO_PLUGINS__0__ENV_PREFIX") + os.Unsetenv("SFTPGO_PLUGINS__0__ENV_VARS") }) err := config.LoadConfig(configDir, "") @@ -739,6 +744,10 @@ func TestPluginsFromEnv(t *testing.T) { require.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme) require.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus) require.Equal(t, 14, pluginConf.AuthOptions.Scope) + require.Equal(t, "prefix_", pluginConf.EnvPrefix) + require.Len(t, pluginConf.EnvVars, 2) + assert.Equal(t, "a", pluginConf.EnvVars[0]) + assert.Equal(t, "b", pluginConf.EnvVars[1]) cfg := make(map[string]any) cfg["plugins"] = pluginConf @@ -754,6 +763,8 @@ func TestPluginsFromEnv(t *testing.T) { os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0") os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeVaultTransit) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusVaultTransit) + os.Setenv("SFTPGO_PLUGINS__0__ENV_PREFIX", "") + os.Setenv("SFTPGO_PLUGINS__0__ENV_VARS", "") err = config.LoadConfig(configDir, confName) assert.NoError(t, err) pluginsConf = config.GetPluginsConfig() @@ -778,6 +789,8 @@ func TestPluginsFromEnv(t *testing.T) { require.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme) require.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus) require.Equal(t, 14, pluginConf.AuthOptions.Scope) + assert.Empty(t, pluginConf.EnvPrefix) + assert.Len(t, pluginConf.EnvVars, 0) err = os.Remove(configFilePath) assert.NoError(t, err) diff --git a/internal/plugin/auth.go b/internal/plugin/auth.go index a9527440..a346f694 100644 --- a/internal/plugin/auth.go +++ b/internal/plugin/auth.go @@ -17,7 +17,6 @@ package plugin import ( "errors" "fmt" - "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" @@ -119,7 +118,8 @@ func (p *authPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: auth.Handshake, Plugins: auth.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, }, diff --git a/internal/plugin/ipfilter.go b/internal/plugin/ipfilter.go index 22a1ba15..29079496 100644 --- a/internal/plugin/ipfilter.go +++ b/internal/plugin/ipfilter.go @@ -16,7 +16,6 @@ package plugin import ( "fmt" - "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" @@ -60,7 +59,8 @@ func (p *ipFilterPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: ipfilter.Handshake, Plugins: ipfilter.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, }, diff --git a/internal/plugin/kms.go b/internal/plugin/kms.go index b697b2da..61a807d1 100644 --- a/internal/plugin/kms.go +++ b/internal/plugin/kms.go @@ -16,7 +16,6 @@ package plugin import ( "fmt" - "os/exec" "path/filepath" "github.com/hashicorp/go-hclog" @@ -81,7 +80,8 @@ func (p *kmsPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: kmsplugin.Handshake, Plugins: kmsplugin.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, }, diff --git a/internal/plugin/metadata.go b/internal/plugin/metadata.go index 9fb56acf..bc1e30fe 100644 --- a/internal/plugin/metadata.go +++ b/internal/plugin/metadata.go @@ -16,7 +16,6 @@ package plugin import ( "fmt" - "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" @@ -60,7 +59,8 @@ func (p *metadataPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: metadata.Handshake, Plugins: metadata.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, }, diff --git a/internal/plugin/notifier.go b/internal/plugin/notifier.go index 92722d63..64f78480 100644 --- a/internal/plugin/notifier.go +++ b/internal/plugin/notifier.go @@ -16,7 +16,6 @@ package plugin import ( "fmt" - "os/exec" "sync" "time" @@ -168,7 +167,8 @@ func (p *notifierPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: notifier.Handshake, Plugins: notifier.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, }, diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index de6f27c7..0b6090e4 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -21,6 +21,10 @@ import ( "encoding/hex" "errors" "fmt" + "os" + "os/exec" + "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -81,6 +85,16 @@ type Config struct { // rejected. The client will also refuse to connect to any server that isn't // the original instance started by the client. AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"` + // EnvPrefix defines the prefix for env vars to pass from the SFTPGo process + // environment to the plugin. Set to "none" to not pass any environment + // variable, set to "*" to pass all environment variables. If empty, the + // prefix is returned as the plugin name in uppercase with "-" replaced with + // "_" and a trailing "_". For example if the plugin name is + // sftpgo-plugin-eventsearch the prefix will be SFTPGO_PLUGIN_EVENTSEARCH_ + EnvPrefix string `json:"env_prefix" mapstructure:"env_prefix"` + // Additional environment variable names to pass from the SFTPGo process + // environment to the plugin. + EnvVars []string `json:"env_vars" mapstructure:"env_vars"` // unique identifier for kms plugins kmsID int } @@ -99,6 +113,42 @@ func (c *Config) getSecureConfig() (*plugin.SecureConfig, error) { return nil, nil } +func (c *Config) getEnvVarPrefix() string { + if c.EnvPrefix == "none" { + return "" + } + if c.EnvPrefix != "" { + return c.EnvPrefix + } + + prefix := strings.ToUpper(filepath.Base(c.Cmd)) + "_" + return strings.ReplaceAll(prefix, "-", "_") +} + +func (c *Config) getCommand() *exec.Cmd { + cmd := exec.Command(c.Cmd, c.Args...) + cmd.Env = []string{} + + if envVarPrefix := c.getEnvVarPrefix(); envVarPrefix != "" { + if envVarPrefix == "*" { + logger.Debug(logSender, "", "sharing all the environment variables with plugin %q", c.Cmd) + cmd.Env = append(cmd.Env, os.Environ()...) + return cmd + } + logger.Debug(logSender, "", "adding env vars with prefix %q for plugin %q", envVarPrefix, c.Cmd) + for _, val := range os.Environ() { + if strings.HasPrefix(val, envVarPrefix) { + cmd.Env = append(cmd.Env, val) + } + } + } + logger.Debug(logSender, "", "additional env vars for plugin %q: %+v", c.Cmd, c.EnvVars) + for _, key := range c.EnvVars { + cmd.Env = append(cmd.Env, os.Getenv(key)) + } + return cmd +} + func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { return &kmsPluginSecretProvider{ BaseSecret: base, diff --git a/internal/plugin/searcher.go b/internal/plugin/searcher.go index 04370d79..de8d6e23 100644 --- a/internal/plugin/searcher.go +++ b/internal/plugin/searcher.go @@ -16,7 +16,6 @@ package plugin import ( "fmt" - "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" @@ -60,7 +59,8 @@ func (p *searcherPlugin) initialize() error { client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: eventsearcher.Handshake, Plugins: eventsearcher.PluginMap, - Cmd: exec.Command(p.config.Cmd, p.config.Args...), + Cmd: p.config.getCommand(), + SkipHostEnv: true, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC, },