diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index b7fe89372edf..eadce0772631 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -101,7 +101,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) sys := vault.TestDynamicSystemView(cores[0].Core) - vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", "TestBackend_PluginMain") + vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", "TestBackend_PluginMain", []string{}, "") return cluster, sys } diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index 0c20aef1ea04..ff04169819b2 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -94,8 +94,8 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { cores := cluster.Cores sys := vault.TestDynamicSystemView(cores[0].Core) - vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", "TestPlugin_GRPC_Main") - vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin-netRPC", "TestPlugin_NetRPC_Main") + vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", "TestPlugin_GRPC_Main", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin-netRPC", "TestPlugin_NetRPC_Main", []string{}, "") return cluster, sys } diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go index 4667717ae800..f90d9450b65e 100644 --- a/builtin/plugin/backend_test.go +++ b/builtin/plugin/backend_test.go @@ -89,7 +89,7 @@ func testConfig(t *testing.T) (*logical.BackendConfig, func()) { os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain", []string{}, "") return config, func() { cluster.Cleanup() diff --git a/helper/pluginutil/runner.go b/helper/pluginutil/runner.go index 436e169fe883..41b32d946338 100644 --- a/helper/pluginutil/runner.go +++ b/helper/pluginutil/runner.go @@ -43,6 +43,7 @@ type PluginRunner struct { Name string `json:"name" structs:"name"` Command string `json:"command" structs:"command"` Args []string `json:"args" structs:"args"` + Env []string `json:"env" structs:"env"` Sha256 []byte `json:"sha256" structs:"sha256"` Builtin bool `json:"builtin" structs:"builtin"` BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"` @@ -65,6 +66,10 @@ func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) { cmd := exec.Command(r.Command, r.Args...) + + // `env` should always go last to avoid overwriting internal values that might + // have been provided externally. + cmd.Env = append(cmd.Env, r.Env...) cmd.Env = append(cmd.Env, env...) // Add the mlock setting to the ENV of the plugin diff --git a/http/plugin_test.go b/http/plugin_test.go index 3fb55dd520bb..bdedc0e699bb 100644 --- a/http/plugin_test.go +++ b/http/plugin_test.go @@ -50,7 +50,7 @@ func getPluginClusterAndCore(t testing.TB, logger log.Logger) (*vault.TestCluste os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) vault.TestWaitActive(t, core.Core) - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestPlugin_PluginMain") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestPlugin_PluginMain", []string{}, "") // Mount the mock plugin err = core.Client.Sys().Mount("mock", &api.MountInput{ diff --git a/vault/logical_system.go b/vault/logical_system.go index db1cd193fc11..9ef54e194dc0 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -269,7 +269,7 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi return logical.ErrorResponse("missing command value"), nil } - // For backwards compatibility, also accept args as part of command. Don't + // For backwards compatibility, also accept args as part of command. Don't // accepts args in both command and args. args := d.Get("args").([]string) parts := strings.Split(command, " ") @@ -281,12 +281,14 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi args = parts[1:] } + env := d.Get("env").([]string) + sha256Bytes, err := hex.DecodeString(sha256) if err != nil { return logical.ErrorResponse("Could not decode SHA-256 value from Hex"), err } - err = b.Core.pluginCatalog.Set(ctx, pluginName, parts[0], args, sha256Bytes) + err = b.Core.pluginCatalog.Set(ctx, pluginName, parts[0], args, env, sha256Bytes) if err != nil { return nil, err } @@ -3526,6 +3528,11 @@ plugin directory.`, `The args passed to plugin command.`, "", }, + "plugin-catalog_env": { + `The environment variables passed to plugin command. +Each entry is of the form "key=value".`, + "", + }, "leases": { `View or list lease metadata.`, ` diff --git a/vault/logical_system_integ_test.go b/vault/logical_system_integ_test.go index 6f0ae0826164..4b03344c25fe 100644 --- a/vault/logical_system_integ_test.go +++ b/vault/logical_system_integ_test.go @@ -20,6 +20,11 @@ import ( "github.com/hashicorp/vault/vault" ) +const ( + expectedEnvKey = "FOO" + expectedEnvValue = "BAR" +) + func TestSystemBackend_Plugin_secret(t *testing.T) { cluster := testSystemBackendMock(t, 1, 1, logical.TypeLogical) defer cluster.Cleanup() @@ -103,7 +108,7 @@ func TestSystemBackend_Plugin_MismatchType(t *testing.T) { core := cluster.Cores[0] // Replace the plugin with a credential backend - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", []string{}, "") // Make a request to lazy load the now-credential plugin // and expect an error @@ -178,7 +183,7 @@ func testPlugin_CatalogRemoved(t *testing.T, btype logical.BackendType, testMoun switch btype { case logical.TypeLogical: // Add plugin back to the catalog - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical", []string{}, "") _, err = core.Client.Logical().Write("sys/mounts/mock-0", map[string]interface{}{ "type": "plugin", "config": map[string]interface{}{ @@ -187,7 +192,7 @@ func testPlugin_CatalogRemoved(t *testing.T, btype logical.BackendType, testMoun }) case logical.TypeCredential: // Add plugin back to the catalog - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", []string{}, "") _, err = core.Client.Logical().Write("sys/auth/mock-0", map[string]interface{}{ "type": "plugin", "plugin_name": "mock-plugin", @@ -283,9 +288,9 @@ func testPlugin_continueOnError(t *testing.T, btype logical.BackendType, mismatc // Re-add the plugin to the catalog switch btype { case logical.TypeLogical: - vault.TestAddTestPluginTempDir(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical", cluster.TempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical", []string{}, cluster.TempDir) case logical.TypeCredential: - vault.TestAddTestPluginTempDir(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", cluster.TempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", []string{}, cluster.TempDir) } // Reload the plugin @@ -480,7 +485,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo switch backendType { case logical.TypeLogical: - vault.TestAddTestPluginTempDir(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical", tempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainLogical", []string{}, tempDir) for i := 0; i < numMounts; i++ { // Alternate input styles for plugin_name on every other mount options := map[string]interface{}{ @@ -502,7 +507,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo } } case logical.TypeCredential: - vault.TestAddTestPluginTempDir(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", tempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainCredentials", []string{}, tempDir) for i := 0; i < numMounts; i++ { // Alternate input styles for plugin_name on every other mount options := map[string]interface{}{ @@ -530,6 +535,58 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo return cluster } +func TestSystemBackend_Plugin_Env(t *testing.T) { + kvPair := fmt.Sprintf("%s=%s", expectedEnvKey, expectedEnvValue) + cluster := testSystemBackend_SingleCluster_Env(t, []string{kvPair}) + defer cluster.Cleanup() +} + +// testSystemBackend_SingleCluster_Env is a helper func that returns a single +// cluster and a single mounted plugin logical backend. +func testSystemBackend_SingleCluster_Env(t *testing.T, env []string) *vault.TestCluster { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "plugin": plugin.Factory, + }, + } + + // Create a tempdir, cluster.Cleanup will clean up this directory + tempDir, err := ioutil.TempDir("", "vault-test-cluster") + if err != nil { + t.Fatal(err) + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + KeepStandbysSealed: true, + NumCores: 1, + TempDir: tempDir, + }) + cluster.Start() + + core := cluster.Cores[0] + vault.TestWaitActive(t, core.Core) + client := core.Client + + os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) + + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMainEnv", env, tempDir) + options := map[string]interface{}{ + "type": "plugin", + "plugin_name": "mock-plugin", + } + + resp, err := client.Logical().Write("sys/mounts/mock", options) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + return cluster +} + func TestBackend_PluginMainLogical(t *testing.T) { args := []string{} if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadataModeEnv) != "true" { @@ -588,6 +645,41 @@ func TestBackend_PluginMainCredentials(t *testing.T) { } } +// TestBackend_PluginMainEnv is a mock plugin that simply checks for the existence of FOO env var. +func TestBackend_PluginMainEnv(t *testing.T) { + actual := os.Getenv(expectedEnvKey) + if actual != expectedEnvValue { + t.Fatalf("expected: %q, got: %q", expectedEnvValue, actual) + } + + args := []string{} + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" && os.Getenv(pluginutil.PluginMetadataModeEnv) != "true" { + return + } + + caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv) + if caPEM == "" { + t.Fatal("CA cert not passed in") + } + args = append(args, fmt.Sprintf("--ca-cert=%s", caPEM)) + + apiClientMeta := &pluginutil.APIClientMeta{} + flags := apiClientMeta.FlagSet() + flags.Parse(args) + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + + factoryFunc := mock.FactoryType(logical.TypeLogical) + + err := lplugin.Serve(&lplugin.ServeOpts{ + BackendFactoryFunc: factoryFunc, + TLSProviderFunc: tlsProviderFunc, + }) + if err != nil { + t.Fatal(err) + } +} + func TestSystemBackend_InternalUIResultantACL(t *testing.T) { cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 1a33c5618f57..3f9c2f0d86f3 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -293,6 +293,10 @@ func (b *SystemBackend) pluginsCatalogPath() *framework.Path { Type: framework.TypeStringSlice, Description: strings.TrimSpace(sysHelp["plugin-catalog_args"][0]), }, + "env": &framework.FieldSchema{ + Type: framework.TypeStringSlice, + Description: strings.TrimSpace(sysHelp["plugin-catalog_env"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index 633b7c341def..f7a25619fe32 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -87,7 +87,7 @@ func (c *PluginCatalog) Get(ctx context.Context, name string) (*pluginutil.Plugi // Set registers a new external plugin with the catalog, or updates an existing // external plugin. It takes the name, command and SHA256 of the plugin. -func (c *PluginCatalog) Set(ctx context.Context, name, command string, args []string, sha256 []byte) error { +func (c *PluginCatalog) Set(ctx context.Context, name, command string, args []string, env []string, sha256 []byte) error { if c.directory == "" { return ErrDirectoryNotConfigured } @@ -122,6 +122,7 @@ func (c *PluginCatalog) Set(ctx context.Context, name, command string, args []st Name: name, Command: command, Args: args, + Env: env, Sha256: sha256, Builtin: false, } diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index a99cac8c97ea..7222959cc954 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -52,7 +52,7 @@ func TestPluginCatalog_CRUD(t *testing.T) { defer file.Close() command := fmt.Sprintf("%s", filepath.Base(file.Name())) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", command, []string{"--test"}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", command, []string{"--test"}, []string{"FOO=BAR"}, []byte{'1'}) if err != nil { t.Fatal(err) } @@ -67,6 +67,7 @@ func TestPluginCatalog_CRUD(t *testing.T) { Name: "mysql-database-plugin", Command: filepath.Join(sym, filepath.Base(file.Name())), Args: []string{"--test"}, + Env: []string{"FOO=BAR"}, Sha256: []byte{'1'}, Builtin: false, } @@ -141,13 +142,13 @@ func TestPluginCatalog_List(t *testing.T) { defer file.Close() command := filepath.Base(file.Name()) - err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", command, []string{"--test"}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "mysql-database-plugin", command, []string{"--test"}, []string{}, []byte{'1'}) if err != nil { t.Fatal(err) } // Set another plugin - err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", command, []string{"--test"}, []byte{'1'}) + err = core.pluginCatalog.Set(context.Background(), "aaaaaaa", command, []string{"--test"}, []string{}, []byte{'1'}) if err != nil { t.Fatal(err) } diff --git a/vault/testing.go b/vault/testing.go index 32c997ae79dc..8341c83fcbc2 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -347,79 +347,49 @@ func TestDynamicSystemView(c *Core) *dynamicSystemView { } // TestAddTestPlugin registers the testFunc as part of the plugin command to the -// plugin catalog. -func TestAddTestPlugin(t testing.T, c *Core, name, testFunc string) { +// plugin catalog. If provided, uses tmpDir as the plugin directory. +func TestAddTestPlugin(t testing.T, c *Core, name, testFunc string, env []string, tempDir string) { file, err := os.Open(os.Args[0]) if err != nil { t.Fatal(err) } defer file.Close() - hash := sha256.New() - - _, err = io.Copy(hash, file) - if err != nil { - t.Fatal(err) - } - - sum := hash.Sum(nil) - - // Determine plugin directory path - fullPath, err := filepath.EvalSymlinks(os.Args[0]) - if err != nil { - t.Fatal(err) - } - directoryPath := filepath.Dir(fullPath) + dirPath := filepath.Dir(os.Args[0]) + fileName := filepath.Base(os.Args[0]) - // Set core's plugin directory and plugin catalog directory - c.pluginDirectory = directoryPath - c.pluginCatalog.directory = directoryPath - - command := fmt.Sprintf("%s", filepath.Base(os.Args[0])) - args := []string{fmt.Sprintf("--test.run=%s", testFunc)} - err = c.pluginCatalog.Set(context.Background(), name, command, args, sum) - if err != nil { - t.Fatal(err) - } -} - -// TestAddTestPluginTempDir registers the testFunc as part of the plugin command to the -// plugin catalog. It uses tmpDir as the plugin directory. -func TestAddTestPluginTempDir(t testing.T, c *Core, name, testFunc, tempDir string) { - file, err := os.Open(os.Args[0]) - if err != nil { - t.Fatal(err) - } - defer file.Close() + if tempDir != "" { + fi, err := file.Stat() + if err != nil { + t.Fatal(err) + } - fi, err := file.Stat() - if err != nil { - t.Fatal(err) - } + // Copy over the file to the temp dir + dst := filepath.Join(tempDir, fileName) + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) + if err != nil { + t.Fatal(err) + } + defer out.Close() - // Copy over the file to the temp dir - dst := filepath.Join(tempDir, filepath.Base(os.Args[0])) - out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) - if err != nil { - t.Fatal(err) - } - defer out.Close() + if _, err = io.Copy(out, file); err != nil { + t.Fatal(err) + } + err = out.Sync() + if err != nil { + t.Fatal(err) + } - if _, err = io.Copy(out, file); err != nil { - t.Fatal(err) - } - err = out.Sync() - if err != nil { - t.Fatal(err) + dirPath = tempDir } - // Determine plugin directory full path - fullPath, err := filepath.EvalSymlinks(tempDir) + // Determine plugin directory full path, evaluating potential symlink path + fullPath, err := filepath.EvalSymlinks(dirPath) if err != nil { t.Fatal(err) } - reader, err := os.Open(filepath.Join(fullPath, filepath.Base(os.Args[0]))) + reader, err := os.Open(filepath.Join(fullPath, fileName)) if err != nil { t.Fatal(err) } @@ -439,9 +409,8 @@ func TestAddTestPluginTempDir(t testing.T, c *Core, name, testFunc, tempDir stri c.pluginDirectory = fullPath c.pluginCatalog.directory = fullPath - command := fmt.Sprintf("%s", filepath.Base(os.Args[0])) args := []string{fmt.Sprintf("--test.run=%s", testFunc)} - err = c.pluginCatalog.Set(context.Background(), name, command, args, sum) + err = c.pluginCatalog.Set(context.Background(), name, fileName, args, env, sum) if err != nil { t.Fatal(err) } diff --git a/website/source/api/system/plugins-catalog.html.md b/website/source/api/system/plugins-catalog.html.md index ee66673aee4e..6f24ec5e6981 100644 --- a/website/source/api/system/plugins-catalog.html.md +++ b/website/source/api/system/plugins-catalog.html.md @@ -67,8 +67,15 @@ supplied name. they do not match the plugin can not be run. - `command` `(string: )` – Specifies the command used to execute the - plugin. This is relative to the plugin directory. e.g. `"myplugin - --my_flag=1"` + plugin. This is relative to the plugin directory. e.g. `"myplugin"`. + +- `args` `(array: [])` – Specifies the arguments used to execute the plugin. If + the arguments are provided here, the `command` parameter should only contain + the named program. e.g. `"--my_flag=1"`. + +- `env` `(array: [])` – Specifies the environment variables used during the + execution of the plugin. Each entry is of the form "key=value". e.g + `"FOO=BAR"`. ### Sample Payload