27bb03bbc0
* adding copyright header * fix fmt and a test
993 lines
32 KiB
Go
993 lines
32 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
|
|
"github.com/hashicorp/vault/helper/testhelpers/pluginhelpers"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/hashicorp/vault/sdk/plugin"
|
|
"github.com/hashicorp/vault/sdk/plugin/mock"
|
|
"github.com/hashicorp/vault/version"
|
|
)
|
|
|
|
const vaultTestingMockPluginEnv = "VAULT_TESTING_MOCK_PLUGIN"
|
|
|
|
// version is used to override the plugin's self-reported version
|
|
func testCoreWithPlugins(t *testing.T, typ consts.PluginType, versions ...string) (*Core, []pluginhelpers.TestPlugin) {
|
|
t.Helper()
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
var plugins []pluginhelpers.TestPlugin
|
|
for _, version := range versions {
|
|
plugins = append(plugins, pluginhelpers.CompilePlugin(t, typ, version, pluginDir))
|
|
}
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
core := TestCoreWithSealAndUI(t, conf)
|
|
core, _, _ = testCoreUnsealed(t, core)
|
|
return core, plugins
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
routerPath string
|
|
expectedMatch string
|
|
}{
|
|
"enable external credential plugin": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
coreConfig := &CoreConfig{
|
|
DisableMlock: true,
|
|
DisableCache: true,
|
|
Logger: log.NewNullLogger(),
|
|
CredentialBackends: map[string]logical.Factory{},
|
|
}
|
|
|
|
cluster := NewTestCluster(t, coreConfig, &TestClusterOptions{
|
|
Plugins: &TestPluginConfig{
|
|
Typ: tc.pluginType,
|
|
Versions: []string{""},
|
|
},
|
|
})
|
|
|
|
cluster.Start()
|
|
defer cluster.Cleanup()
|
|
|
|
c := cluster.Cores[0].Core
|
|
TestWaitActive(t, c)
|
|
|
|
plugins := cluster.Plugins
|
|
|
|
registerPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType.String(), "1.0.0", plugins[0].Sha256, plugins[0].FileName)
|
|
|
|
mountPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType, "v1.0.0", "")
|
|
|
|
match := c.router.MatchingMount(namespace.RootContext(nil), tc.routerPath)
|
|
if match != tc.expectedMatch {
|
|
t.Fatalf("missing mount, match: %q", match)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin_MultipleVersions(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
registerVersions []string
|
|
mountVersion string
|
|
expectedVersion string
|
|
routerPath string
|
|
expectedMatch string
|
|
}{
|
|
"enable external credential plugin, multiple versions available": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "v1.0.0",
|
|
expectedVersion: "v1.0.0",
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin, multiple versions available": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "v1.0.0",
|
|
expectedVersion: "v1.0.0",
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
"enable external credential plugin, multiple versions available, select other version": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "v1.0.1",
|
|
expectedVersion: "v1.0.1",
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin, multiple versions available, select other version": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "v1.0.1",
|
|
expectedVersion: "v1.0.1",
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
"enable external credential plugin, selects latest when version not specified": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "",
|
|
expectedVersion: "v1.0.1",
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin, selects latest when version not specified": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
registerVersions: []string{"v1.0.0", "v1.0.1"},
|
|
mountVersion: "",
|
|
expectedVersion: "v1.0.1",
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, "")
|
|
for _, version := range tc.registerVersions {
|
|
registerPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType.String(), version, plugins[0].Sha256, plugins[0].FileName)
|
|
}
|
|
|
|
mountPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType, tc.mountVersion, "")
|
|
|
|
match := c.router.MatchingMount(namespace.RootContext(nil), tc.routerPath)
|
|
if match != tc.expectedMatch {
|
|
t.Fatalf("missing mount, match: %q", match)
|
|
}
|
|
|
|
raw, _ := c.router.root.Get(match)
|
|
if raw.(*routeEntry).mountEntry.Version != tc.expectedVersion {
|
|
t.Errorf("Expected mount to be version %s but got %s", tc.expectedVersion, raw.(*routeEntry).mountEntry.Version)
|
|
}
|
|
|
|
if raw.(*routeEntry).mountEntry.RunningVersion != tc.expectedVersion {
|
|
t.Errorf("Expected mount running version to be %s but got %s", tc.expectedVersion, raw.(*routeEntry).mountEntry.RunningVersion)
|
|
}
|
|
|
|
if raw.(*routeEntry).mountEntry.RunningSha256 == "" {
|
|
t.Errorf("Expected RunningSha256 to be present: %+v", raw.(*routeEntry).mountEntry.RunningSha256)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin_Deregister_SealUnseal(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// create an external plugin to shadow the builtin "pending-removal-test-plugin"
|
|
pluginName := "therug"
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeCredential, "", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, pluginName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, keys, root := testCoreUnsealed(t, c)
|
|
|
|
// Register a plugin
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "", plugin.Sha256, plugin.FileName)
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
plugct := len(c.pluginCatalog.externalPlugins)
|
|
if plugct != 1 {
|
|
t.Fatalf("expected a single external plugin entry after registering, got: %d", plugct)
|
|
}
|
|
|
|
// Now pull the rug out from underneath us
|
|
deregisterPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "", "", "")
|
|
|
|
if err := c.Seal(root); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
for i, key := range keys {
|
|
unseal, err := TestCoreUnseal(c, key)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if i+1 == len(keys) && !unseal {
|
|
t.Fatalf("err: should be unsealed")
|
|
}
|
|
}
|
|
|
|
plugct = len(c.pluginCatalog.externalPlugins)
|
|
if plugct != 0 {
|
|
t.Fatalf("expected no plugin entries after unseal, got: %d", plugct)
|
|
}
|
|
|
|
found := false
|
|
mounts, err := c.ListAuths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, mount := range mounts {
|
|
if mount.Type == pluginName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf("expected to find %s mount, but got none", pluginName)
|
|
}
|
|
}
|
|
|
|
// TestCore_Unseal_isMajorVersionFirstMount_PendingRemoval_Plugin tests the
|
|
// behavior of deprecated builtins when attempting to unseal Vault after a major
|
|
// version upgrade. It simulates this behavior by instantiating a Vault cluster,
|
|
// registering a shadow plugin to mount a builtin, and deregistering the shadow
|
|
// plugin. The first unseal should work. Before sealing and unsealing again, the
|
|
// version store is cleared. Vault sees the next unseal as a major upgrade and
|
|
// should immediately shut down.
|
|
func TestCore_Unseal_isMajorVersionFirstMount_PendingRemoval_Plugin(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// create an external plugin to shadow the builtin "pending-removal-test-plugin"
|
|
pluginName := "pending-removal-test-plugin"
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeCredential, "", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, pluginName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, keys, root := testCoreUnsealed(t, c)
|
|
|
|
// Register a shadow plugin
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "", plugin.Sha256, plugin.FileName)
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
|
|
// Deregister shadow plugin
|
|
deregisterPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "", plugin.Sha256, plugin.FileName)
|
|
|
|
// Make sure this isn't the first mount for the current major version.
|
|
if c.isMajorVersionFirstMount(context.Background()) {
|
|
t.Fatalf("expected major version to register as mounted")
|
|
}
|
|
|
|
if err := c.Seal(root); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
for i, key := range keys {
|
|
unseal, err := TestCoreUnseal(c, key)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if i+1 == len(keys) && !unseal {
|
|
t.Fatalf("err: should be unsealed")
|
|
}
|
|
}
|
|
|
|
// Now remove version history and try again
|
|
vaultVersionPath := "core/versions/"
|
|
key := vaultVersionPath + version.Version
|
|
if err := c.barrier.Delete(context.Background(), key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// loadVersionHistory doesn't care about invalidating old entries, since
|
|
// they shouldn't really be deleted from the version store. It just updates
|
|
// the map, so we need to manually delete the current entry.
|
|
delete(c.versionHistory, version.Version)
|
|
|
|
// Make sure this appears to be the first mount for the current major
|
|
// version.
|
|
if !c.isMajorVersionFirstMount(context.Background()) {
|
|
t.Fatalf("expected major version first mount")
|
|
}
|
|
|
|
// Seal again and check for unseal failure.
|
|
if err := c.Seal(root); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
for i, key := range keys {
|
|
unseal, err := TestCoreUnseal(c, key)
|
|
if i+1 == len(keys) {
|
|
if !errors.Is(err, errLoadAuthFailed) {
|
|
t.Fatalf("expected error: %q, got: %q", errLoadAuthFailed, err)
|
|
}
|
|
|
|
if unseal {
|
|
t.Fatalf("err: should not be unsealed")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin_PendingRemoval(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// create an external plugin to shadow the builtin "pending-removal-test-plugin"
|
|
pluginName := "pending-removal-test-plugin"
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeCredential, "v1.2.3", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, pluginName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, _, _ = testCoreUnsealed(t, c)
|
|
|
|
pendingRemovalString := "pending removal"
|
|
|
|
// Create a new auth method with builtin pending-removal-test-plugin
|
|
resp, err := mountPluginWithResponse(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
if err == nil {
|
|
t.Fatalf("expected error when mounting deprecated backend")
|
|
}
|
|
if resp == nil || resp.Data == nil || !strings.Contains(resp.Data["error"].(string), pendingRemovalString) {
|
|
t.Fatalf("expected error response to contain %q but got %+v", pendingRemovalString, resp)
|
|
}
|
|
|
|
// Register a shadow plugin
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "v1.2.3", plugin.Sha256, plugin.FileName)
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin_ShadowBuiltin(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// create an external plugin to shadow the builtin "approle"
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeCredential, "v1.2.3", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, "approle"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pluginName := "approle"
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, _, _ = testCoreUnsealed(t, c)
|
|
|
|
verifyAuthListDeprecationStatus := func(authName string, checkExists bool) error {
|
|
req := logical.TestRequest(t, logical.ReadOperation, mountTable(consts.PluginTypeCredential))
|
|
req.Data = map[string]interface{}{
|
|
"type": authName,
|
|
}
|
|
resp, err := c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status := resp.Data["deprecation_status"]
|
|
if checkExists && status == nil {
|
|
return fmt.Errorf("expected deprecation status but found none")
|
|
} else if !checkExists && status != nil {
|
|
return fmt.Errorf("expected nil deprecation status but found %q", status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create a new auth method with builtin approle
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
|
|
// Read the auth table to verify deprecation status
|
|
if err := verifyAuthListDeprecationStatus(pluginName, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Register a shadow plugin
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "v1.2.3", plugin.Sha256, plugin.FileName)
|
|
|
|
// Verify auth table hasn't changed
|
|
if err := verifyAuthListDeprecationStatus(pluginName, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Remount auth method using registered shadow plugin
|
|
unmountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
|
|
// Verify auth table has changed
|
|
if err := verifyAuthListDeprecationStatus(pluginName, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Deregister shadow plugin
|
|
deregisterPlugin(t, c.systemBackend, pluginName, consts.PluginTypeSecrets.String(), "v1.2.3", plugin.Sha256, plugin.FileName)
|
|
|
|
// Verify auth table hasn't changed
|
|
if err := verifyAuthListDeprecationStatus(pluginName, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Remount auth method
|
|
unmountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
mountPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential, "", "")
|
|
|
|
// Verify auth table has changed
|
|
if err := verifyAuthListDeprecationStatus(pluginName, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalKv_MultipleVersions(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// new kv plugin can be registered but not mounted
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeSecrets, "v1.2.3", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, "kv"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pluginName := "kv"
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, _, _ = testCoreUnsealed(t, c)
|
|
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeSecrets.String(), "v1.2.3", plugin.Sha256, plugin.FileName)
|
|
req := logical.TestRequest(t, logical.ReadOperation, "plugins/catalog")
|
|
resp, err := c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() != nil {
|
|
t.Fatalf("%#v", resp)
|
|
}
|
|
found := false
|
|
for _, plugin := range resp.Data["detailed"].([]map[string]any) {
|
|
if plugin["name"] == pluginName && plugin["version"] == "v1.2.3" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("Expected to find v1.2.3 kv plugin but did not")
|
|
}
|
|
req = logical.TestRequest(t, logical.UpdateOperation, mountTable(consts.PluginTypeSecrets))
|
|
req.Data = map[string]interface{}{
|
|
"type": pluginName,
|
|
}
|
|
req.Data["config"] = map[string]interface{}{
|
|
"plugin_version": "v1.2.3",
|
|
}
|
|
resp, err = c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() == nil {
|
|
t.Fatal("Expected resp error but got successful response")
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalNoop_MultipleVersions(t *testing.T) {
|
|
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
|
|
t.Cleanup(func() { cleanup(t) })
|
|
|
|
// new noop plugin can be registered but not mounted
|
|
plugin := pluginhelpers.CompilePlugin(t, consts.PluginTypeCredential, "v1.2.3", pluginDir)
|
|
err := os.Link(path.Join(pluginDir, plugin.FileName), path.Join(pluginDir, "noop"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pluginName := "noop"
|
|
conf := &CoreConfig{
|
|
BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(),
|
|
PluginDirectory: pluginDir,
|
|
}
|
|
c := TestCoreWithSealAndUI(t, conf)
|
|
c, _, _ = testCoreUnsealed(t, c)
|
|
|
|
registerPlugin(t, c.systemBackend, pluginName, consts.PluginTypeCredential.String(), "v1.2.3", plugin.Sha256, plugin.FileName)
|
|
req := logical.TestRequest(t, logical.ReadOperation, "plugins/catalog")
|
|
resp, err := c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() != nil {
|
|
t.Fatalf("%#v", resp)
|
|
}
|
|
found := false
|
|
for _, plugin := range resp.Data["detailed"].([]map[string]any) {
|
|
if plugin["name"] == "noop" && plugin["version"] == "v1.2.3" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("Expected to find v1.2.3 noop plugin but did not")
|
|
}
|
|
req = logical.TestRequest(t, logical.UpdateOperation, mountTable(consts.PluginTypeCredential))
|
|
req.Data = map[string]interface{}{
|
|
"type": pluginName,
|
|
}
|
|
req.Data["config"] = map[string]interface{}{
|
|
"plugin_version": "v1.2.3",
|
|
}
|
|
resp, err = c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() == nil {
|
|
t.Fatal("Expected resp error but got successful response")
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalPlugin_NoVersionsOkay(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
routerPath string
|
|
expectedMatch string
|
|
}{
|
|
"enable external credential plugin with no version": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin with no version": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, "")
|
|
// When an unversioned plugin is registered, mounting a plugin with no
|
|
// version specified should mount the unversioned plugin even if there
|
|
// are versioned plugins available.
|
|
for _, version := range []string{"", "v1.0.0"} {
|
|
registerPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType.String(), version, plugins[0].Sha256, plugins[0].FileName)
|
|
}
|
|
|
|
mountPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType, "", "")
|
|
|
|
match := c.router.MatchingMount(namespace.RootContext(nil), tc.routerPath)
|
|
if match != tc.expectedMatch {
|
|
t.Fatalf("missing mount, match: %q", match)
|
|
}
|
|
|
|
raw, _ := c.router.root.Get(match)
|
|
if raw.(*routeEntry).mountEntry.Version != "" {
|
|
t.Errorf("Expected mount to be empty version but got %s", raw.(*routeEntry).mountEntry.Version)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalCredentialPlugin_NoVersionOnRegister(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
routerPath string
|
|
expectedMatch string
|
|
}{
|
|
"enable external credential plugin with version, but no version was provided on registration": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
routerPath: "auth/foo/bar",
|
|
expectedMatch: "auth/foo/",
|
|
},
|
|
"enable external secrets plugin with version, but no version was provided on registration": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
routerPath: "foo/bar",
|
|
expectedMatch: "foo/",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, "")
|
|
registerPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType.String(), "", plugins[0].Sha256, plugins[0].FileName)
|
|
|
|
req := logical.TestRequest(t, logical.UpdateOperation, mountTable(tc.pluginType))
|
|
req.Data = map[string]interface{}{
|
|
"type": plugins[0].Name,
|
|
"config": map[string]interface{}{
|
|
"plugin_version": "v1.0.0",
|
|
},
|
|
}
|
|
resp, _ := c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if resp == nil || !resp.IsError() || !strings.Contains(resp.Error().Error(), ErrPluginNotFound.Error()) {
|
|
t.Fatalf("Expected to get plugin not found but got: %v", resp.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCore_EnableExternalCredentialPlugin_InvalidName(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
}{
|
|
"enable external credential plugin with the wrong name": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
},
|
|
"enable external secrets plugin with the wrong name": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, "")
|
|
d := &framework.FieldData{
|
|
Raw: map[string]interface{}{
|
|
"name": plugins[0].Name,
|
|
"sha256": plugins[0].Sha256,
|
|
"version": "v1.0.0",
|
|
"command": plugins[0].Name + "xyz",
|
|
},
|
|
Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields,
|
|
}
|
|
_, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d)
|
|
if err == nil || !strings.Contains(err.Error(), "no such file or directory") {
|
|
t.Fatalf("should have gotten a no such file or directory error inserting the plugin: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalPlugin_getBackendTypeVersion(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
pluginType consts.PluginType
|
|
setRunningVersion string
|
|
}{
|
|
"external credential plugin": {
|
|
pluginType: consts.PluginTypeCredential,
|
|
setRunningVersion: "v1.2.3",
|
|
},
|
|
"external secrets plugin": {
|
|
pluginType: consts.PluginTypeSecrets,
|
|
setRunningVersion: "v1.2.3",
|
|
},
|
|
"external database plugin": {
|
|
pluginType: consts.PluginTypeDatabase,
|
|
setRunningVersion: "v1.2.3",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, tc.setRunningVersion)
|
|
registerPlugin(t, c.systemBackend, plugins[0].Name, tc.pluginType.String(), tc.setRunningVersion, plugins[0].Sha256, plugins[0].FileName)
|
|
|
|
shaBytes, _ := hex.DecodeString(plugins[0].Sha256)
|
|
commandFull := filepath.Join(c.pluginCatalog.directory, plugins[0].FileName)
|
|
entry := &pluginutil.PluginRunner{
|
|
Name: plugins[0].Name,
|
|
Command: commandFull,
|
|
Args: nil,
|
|
Sha256: shaBytes,
|
|
Builtin: false,
|
|
}
|
|
|
|
var version logical.PluginVersion
|
|
var err error
|
|
if tc.pluginType == consts.PluginTypeDatabase {
|
|
version, err = c.pluginCatalog.getDatabaseRunningVersion(context.Background(), entry)
|
|
} else {
|
|
version, err = c.pluginCatalog.getBackendRunningVersion(context.Background(), entry)
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if version.Version != tc.setRunningVersion {
|
|
t.Errorf("Expected to get version %v but got %v", tc.setRunningVersion, version.Version)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalPlugin_CheckFilePermissions(t *testing.T) {
|
|
// Turn on the check.
|
|
if err := os.Setenv(consts.VaultEnableFilePermissionsCheckEnv, "true"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := os.Unsetenv(consts.VaultEnableFilePermissionsCheckEnv); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
for name, tc := range map[string]struct {
|
|
pluginNameFmt string
|
|
pluginType consts.PluginType
|
|
pluginVersion string
|
|
}{
|
|
"plugin name and file name match": {
|
|
pluginNameFmt: "%s",
|
|
pluginType: consts.PluginTypeCredential,
|
|
},
|
|
"plugin name and file name mismatch": {
|
|
pluginNameFmt: "%s-foo",
|
|
pluginType: consts.PluginTypeSecrets,
|
|
},
|
|
"plugin name has slash": {
|
|
pluginNameFmt: "%s/foo",
|
|
pluginType: consts.PluginTypeCredential,
|
|
},
|
|
"plugin with version": {
|
|
pluginNameFmt: "%s/foo",
|
|
pluginType: consts.PluginTypeCredential,
|
|
pluginVersion: "v1.2.3",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c, plugins := testCoreWithPlugins(t, tc.pluginType, tc.pluginVersion)
|
|
registeredPluginName := fmt.Sprintf(tc.pluginNameFmt, plugins[0].Name)
|
|
|
|
// Permissions will be checked once during registration.
|
|
req := logical.TestRequest(t, logical.UpdateOperation, fmt.Sprintf("plugins/catalog/%s/%s", tc.pluginType.String(), registeredPluginName))
|
|
req.Data = map[string]interface{}{
|
|
"command": plugins[0].FileName,
|
|
"sha256": plugins[0].Sha256,
|
|
"version": tc.pluginVersion,
|
|
}
|
|
resp, err := c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() != nil {
|
|
t.Fatal(resp.Error())
|
|
}
|
|
|
|
// Now attempt to mount the plugin, which should trigger checking the permissions again.
|
|
req = logical.TestRequest(t, logical.UpdateOperation, mountTable(tc.pluginType))
|
|
req.Data = map[string]interface{}{
|
|
"type": registeredPluginName,
|
|
}
|
|
if tc.pluginVersion != "" {
|
|
req.Data["config"] = map[string]interface{}{
|
|
"plugin_version": tc.pluginVersion,
|
|
}
|
|
}
|
|
resp, err = c.systemBackend.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error() != nil {
|
|
t.Fatal(resp.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExternalPlugin_DifferentVersionsAndArgs_AreNotMultiplexed(t *testing.T) {
|
|
env := []string{fmt.Sprintf("%s=yes", vaultTestingMockPluginEnv)}
|
|
core, _, _ := TestCoreUnsealed(t)
|
|
|
|
for i, tc := range []struct {
|
|
version string
|
|
testName string
|
|
}{
|
|
{"v1.2.3", "TestBackend_PluginMain_Multiplexed_Logical_v123"},
|
|
{"v1.2.4", "TestBackend_PluginMain_Multiplexed_Logical_v124"},
|
|
} {
|
|
// Register and mount plugins.
|
|
TestAddTestPlugin(t, core, "mux-secret", consts.PluginTypeSecrets, tc.version, tc.testName, env, "")
|
|
mountPlugin(t, core.systemBackend, "mux-secret", consts.PluginTypeSecrets, tc.version, fmt.Sprintf("foo%d", i))
|
|
}
|
|
|
|
if len(core.pluginCatalog.externalPlugins) != 2 {
|
|
t.Fatalf("expected 2 external plugins, but got %d", len(core.pluginCatalog.externalPlugins))
|
|
}
|
|
}
|
|
|
|
func TestExternalPlugin_DifferentTypes_AreNotMultiplexed(t *testing.T) {
|
|
const version = "v1.2.3"
|
|
env := []string{fmt.Sprintf("%s=yes", vaultTestingMockPluginEnv)}
|
|
core, _, _ := TestCoreUnsealed(t)
|
|
|
|
// Register and mount plugins.
|
|
TestAddTestPlugin(t, core, "mux-aws", consts.PluginTypeSecrets, version, "TestBackend_PluginMain_Multiplexed_Logical_v123", env, "")
|
|
TestAddTestPlugin(t, core, "mux-aws", consts.PluginTypeCredential, version, "TestBackend_PluginMain_Multiplexed_Credential_v123", env, "")
|
|
|
|
mountPlugin(t, core.systemBackend, "mux-aws", consts.PluginTypeSecrets, version, "")
|
|
mountPlugin(t, core.systemBackend, "mux-aws", consts.PluginTypeCredential, version, "")
|
|
|
|
if len(core.pluginCatalog.externalPlugins) != 2 {
|
|
t.Fatalf("expected 2 external plugins, but got %d", len(core.pluginCatalog.externalPlugins))
|
|
}
|
|
}
|
|
|
|
func TestExternalPlugin_DifferentEnv_AreNotMultiplexed(t *testing.T) {
|
|
const version = "v1.2.3"
|
|
baseEnv := []string{
|
|
fmt.Sprintf("%s=yes", vaultTestingMockPluginEnv),
|
|
}
|
|
alteredEnv := []string{
|
|
fmt.Sprintf("%s=yes", vaultTestingMockPluginEnv),
|
|
"FOO=BAR",
|
|
}
|
|
|
|
core, _, _ := TestCoreUnsealed(t)
|
|
|
|
// Register and mount plugins.
|
|
for i, env := range [][]string{baseEnv, alteredEnv} {
|
|
TestAddTestPlugin(t, core, "mux-secret", consts.PluginTypeSecrets, version, "TestBackend_PluginMain_Multiplexed_Logical_v123", env, "")
|
|
mountPlugin(t, core.systemBackend, "mux-secret", consts.PluginTypeSecrets, version, fmt.Sprintf("foo%d", i))
|
|
}
|
|
|
|
if len(core.pluginCatalog.externalPlugins) != 2 {
|
|
t.Fatalf("expected 2 external plugins, but got %d", len(core.pluginCatalog.externalPlugins))
|
|
}
|
|
}
|
|
|
|
// Used to run a mock multiplexed secrets plugin
|
|
func TestBackend_PluginMain_Multiplexed_Logical_v123(t *testing.T) {
|
|
if os.Getenv(vaultTestingMockPluginEnv) == "" {
|
|
return
|
|
}
|
|
|
|
os.Setenv(mock.MockPluginVersionEnv, "v1.2.3")
|
|
|
|
err := plugin.ServeMultiplex(&plugin.ServeOpts{
|
|
BackendFactoryFunc: mock.FactoryType(logical.TypeLogical),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Used to run a mock multiplexed secrets plugin
|
|
func TestBackend_PluginMain_Multiplexed_Logical_v124(t *testing.T) {
|
|
if os.Getenv(vaultTestingMockPluginEnv) == "" {
|
|
return
|
|
}
|
|
|
|
os.Setenv(mock.MockPluginVersionEnv, "v1.2.4")
|
|
|
|
err := plugin.ServeMultiplex(&plugin.ServeOpts{
|
|
BackendFactoryFunc: mock.FactoryType(logical.TypeLogical),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Used to run a mock multiplexed auth plugin
|
|
func TestBackend_PluginMain_Multiplexed_Credential_v123(t *testing.T) {
|
|
if os.Getenv(vaultTestingMockPluginEnv) == "" {
|
|
return
|
|
}
|
|
|
|
os.Setenv(mock.MockPluginVersionEnv, "v1.2.3")
|
|
|
|
err := plugin.ServeMultiplex(&plugin.ServeOpts{
|
|
BackendFactoryFunc: mock.FactoryType(logical.TypeCredential),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func registerPlugin(t *testing.T, sys *SystemBackend, pluginName, pluginType, version, sha, command string) {
|
|
t.Helper()
|
|
req := logical.TestRequest(t, logical.UpdateOperation, fmt.Sprintf("plugins/catalog/%s/%s", pluginType, pluginName))
|
|
req.Data = map[string]interface{}{
|
|
"command": command,
|
|
"sha256": sha,
|
|
"version": version,
|
|
}
|
|
resp, err := sys.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
}
|
|
|
|
func mountPluginWithResponse(t *testing.T, sys *SystemBackend, pluginName string, pluginType consts.PluginType, version, path string) (*logical.Response, error) {
|
|
t.Helper()
|
|
var mountPath string
|
|
if path == "" {
|
|
mountPath = mountTable(pluginType)
|
|
} else {
|
|
mountPath = mountTableWithPath(consts.PluginTypeSecrets, path)
|
|
}
|
|
req := logical.TestRequest(t, logical.UpdateOperation, mountPath)
|
|
req.Data = map[string]interface{}{
|
|
"type": pluginName,
|
|
}
|
|
if version != "" {
|
|
req.Data["config"] = map[string]interface{}{
|
|
"plugin_version": version,
|
|
}
|
|
}
|
|
return sys.HandleRequest(namespace.RootContext(nil), req)
|
|
}
|
|
|
|
func mountPlugin(t *testing.T, sys *SystemBackend, pluginName string, pluginType consts.PluginType, version, path string) {
|
|
t.Helper()
|
|
resp, err := mountPluginWithResponse(t, sys, pluginName, pluginType, version, path)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
}
|
|
|
|
func unmountPlugin(t *testing.T, sys *SystemBackend, pluginName string, pluginType consts.PluginType, version, path string) {
|
|
t.Helper()
|
|
var mountPath string
|
|
if path == "" {
|
|
mountPath = mountTable(pluginType)
|
|
} else {
|
|
mountPath = mountTableWithPath(consts.PluginTypeSecrets, path)
|
|
}
|
|
req := logical.TestRequest(t, logical.DeleteOperation, mountPath)
|
|
req.Data = map[string]interface{}{
|
|
"type": pluginName,
|
|
}
|
|
if version != "" {
|
|
req.Data["config"] = map[string]interface{}{
|
|
"plugin_version": version,
|
|
}
|
|
}
|
|
resp, err := sys.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
}
|
|
|
|
func deregisterPlugin(t *testing.T, sys *SystemBackend, pluginName, pluginType, version, sha, command string) {
|
|
t.Helper()
|
|
req := logical.TestRequest(t, logical.DeleteOperation, fmt.Sprintf("plugins/catalog/%s/%s", pluginType, pluginName))
|
|
req.Data = map[string]interface{}{
|
|
"command": command,
|
|
"sha256": sha,
|
|
"version": version,
|
|
}
|
|
resp, err := sys.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
}
|
|
|
|
func mountTable(pluginType consts.PluginType) string {
|
|
return mountTableWithPath(pluginType, "foo")
|
|
}
|
|
|
|
func mountTableWithPath(pluginType consts.PluginType, path string) string {
|
|
switch pluginType {
|
|
case consts.PluginTypeCredential:
|
|
return "auth/" + path
|
|
case consts.PluginTypeSecrets:
|
|
return "mounts/" + path
|
|
default:
|
|
panic("test does not support mounting plugin type yet: " + pluginType.String())
|
|
}
|
|
}
|