diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index b79093fe3f..e2e362fd5f 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -336,7 +336,7 @@ func (b *databaseBackend) GetConnectionWithConfig(ctx context.Context, name stri return nil, err } - dbw, err := newDatabaseWrapper(ctx, config.PluginName, b.System(), b.logger) + dbw, err := newDatabaseWrapper(ctx, config.PluginName, config.PluginVersion, b.System(), b.logger) if err != nil { return nil, fmt.Errorf("unable to create database instance: %w", err) } diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index cb56209ccf..191d2a5b93 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -48,12 +48,12 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) sys := vault.TestDynamicSystemView(cores[0].Core, nil) - vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_PostgresMultiplexed", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Mongo", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoMultiplexed", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlas", []string{}, "") - vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlasMultiplexed", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Postgres", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_PostgresMultiplexed", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Mongo", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoMultiplexed", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoAtlas", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MongoAtlasMultiplexed", []string{}, "") return cluster, sys } @@ -236,6 +236,7 @@ func TestBackend_config_connection(t *testing.T) { "allowed_roles": []string{"*"}, "root_credentials_rotate_statements": []string{}, "password_policy": "", + "plugin_version": "", } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -289,6 +290,7 @@ func TestBackend_config_connection(t *testing.T) { "allowed_roles": []string{"*"}, "root_credentials_rotate_statements": []string{}, "password_policy": "", + "plugin_version": "", } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -331,6 +333,7 @@ func TestBackend_config_connection(t *testing.T) { "allowed_roles": []string{"flu", "barre"}, "root_credentials_rotate_statements": []string{}, "password_policy": "", + "plugin_version": "", } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -728,6 +731,7 @@ func TestBackend_connectionCrud(t *testing.T) { "allowed_roles": []string{"plugin-role-test"}, "root_credentials_rotate_statements": []string(nil), "password_policy": "", + "plugin_version": "", } req.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), req) @@ -1503,7 +1507,7 @@ func TestBackend_AsyncClose(t *testing.T) { // Test that having a plugin that takes a LONG time to close will not cause the cleanup function to take // longer than 750ms. cluster, sys := getCluster(t) - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "hanging-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Hanging", []string{}, "") + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "hanging-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_Hanging", []string{}, "") t.Cleanup(cluster.Cleanup) config := logical.TestBackendConfig() diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index 9e86874c9d..bea9e30ec7 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -110,7 +110,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { cores := cluster.Cores sys := vault.TestDynamicSystemView(cores[0].Core, nil) - vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "TestPlugin_GRPC_Main", []string{}, "") + vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "", "TestPlugin_GRPC_Main", []string{}, "") return cluster, sys } @@ -139,7 +139,7 @@ func TestPlugin_Init(t *testing.T) { cluster, sys := getCluster(t) defer cluster.Cleanup() - dbRaw, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger()) + dbRaw, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) if err != nil { t.Fatalf("err: %s", err) } @@ -163,7 +163,7 @@ func TestPlugin_CreateUser(t *testing.T) { cluster, sys := getCluster(t) defer cluster.Cleanup() - db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger()) + db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) if err != nil { t.Fatalf("err: %s", err) } @@ -203,7 +203,7 @@ func TestPlugin_RenewUser(t *testing.T) { cluster, sys := getCluster(t) defer cluster.Cleanup() - db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger()) + db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) if err != nil { t.Fatalf("err: %s", err) } @@ -237,7 +237,7 @@ func TestPlugin_RevokeUser(t *testing.T) { cluster, sys := getCluster(t) defer cluster.Cleanup() - db, err := dbplugin.PluginFactory(namespace.RootContext(nil), "test-plugin", sys, log.NewNullLogger()) + db, err := dbplugin.PluginFactoryVersion(namespace.RootContext(nil), "test-plugin", "", sys, log.NewNullLogger()) if err != nil { t.Fatalf("err: %s", err) } diff --git a/builtin/logical/database/path_config_connection.go b/builtin/logical/database/path_config_connection.go index cfc61a98b8..c0522bf9fb 100644 --- a/builtin/logical/database/path_config_connection.go +++ b/builtin/logical/database/path_config_connection.go @@ -5,12 +5,16 @@ import ( "errors" "fmt" "net/url" + "sort" "github.com/fatih/structs" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-version" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "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" ) @@ -22,7 +26,8 @@ var ( // DatabaseConfig is used by the Factory function to configure a Database // object. type DatabaseConfig struct { - PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"` + PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"` + PluginVersion string `json:"plugin_version" structs:"plugin_version" mapstructure:"plugin_version"` // ConnectionDetails stores the database specific connection settings needed // by each database type. ConnectionDetails map[string]interface{} `json:"connection_details" structs:"connection_details" mapstructure:"connection_details"` @@ -110,6 +115,11 @@ func pathConfigurePluginConnection(b *databaseBackend) *framework.Path { that plugin type.`, }, + "plugin_version": { + Type: framework.TypeString, + Description: `The version of the plugin to use.`, + }, + "verify_connection": { Type: framework.TypeBool, Default: true, @@ -281,6 +291,48 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { return logical.ErrorResponse(respErrEmptyPluginName), nil } + if pluginVersionRaw, ok := data.GetOk("plugin_version"); ok { + config.PluginVersion = pluginVersionRaw.(string) + } + + unversionedPlugin, err := b.System().LookupPlugin(ctx, config.PluginName, consts.PluginTypeDatabase) + switch { + case config.PluginVersion != "": + semanticVersion, err := version.NewVersion(config.PluginVersion) + if err != nil { + return logical.ErrorResponse("version %q is not a valid semantic version: %s", config.PluginVersion, err), nil + } + + // Canonicalize the version. + config.PluginVersion = "v" + semanticVersion.String() + case err == nil && !unversionedPlugin.Builtin: + // We'll select the unversioned plugin that's been registered. + case req.Operation == logical.CreateOperation: + // No version provided and no unversioned plugin of that name available. + // Pin to the current latest version if any versioned plugins are registered. + plugins, err := b.System().ListVersionedPlugins(ctx, consts.PluginTypeDatabase) + if err != nil { + return nil, err + } + + var versionedCandidates []pluginutil.VersionedPlugin + for _, plugin := range plugins { + if !plugin.Builtin && plugin.Name == config.PluginName && plugin.Version != "" { + versionedCandidates = append(versionedCandidates, plugin) + } + } + + if len(versionedCandidates) != 0 { + // Sort in reverse order. + sort.SliceStable(versionedCandidates, func(i, j int) bool { + return versionedCandidates[i].SemanticVersion.GreaterThan(versionedCandidates[j].SemanticVersion) + }) + + config.PluginVersion = "v" + versionedCandidates[0].SemanticVersion.String() + b.logger.Debug(fmt.Sprintf("pinning %q database plugin version %q from candidates %v", config.PluginName, config.PluginVersion, versionedCandidates)) + } + } + if allowedRolesRaw, ok := data.GetOk("allowed_roles"); ok { config.AllowedRoles = allowedRolesRaw.([]string) } else if req.Operation == logical.CreateOperation { @@ -301,6 +353,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { // ConnectionDetails. delete(data.Raw, "name") delete(data.Raw, "plugin_name") + delete(data.Raw, "plugin_version") delete(data.Raw, "allowed_roles") delete(data.Raw, "verify_connection") delete(data.Raw, "root_rotation_statements") @@ -326,7 +379,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { } // Create a database plugin and initialize it. - dbw, err := newDatabaseWrapper(ctx, config.PluginName, b.System(), b.logger) + dbw, err := newDatabaseWrapper(ctx, config.PluginName, config.PluginVersion, b.System(), b.logger) if err != nil { return logical.ErrorResponse("error creating database object: %s", err), nil } diff --git a/builtin/logical/database/version_wrapper.go b/builtin/logical/database/version_wrapper.go index 3dc6c69a59..2e45063902 100644 --- a/builtin/logical/database/version_wrapper.go +++ b/builtin/logical/database/version_wrapper.go @@ -20,8 +20,8 @@ type databaseVersionWrapper struct { // newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object // that can be used to make operations on the underlying database plugin. -func newDatabaseWrapper(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) { - newDB, err := v5.PluginFactory(ctx, pluginName, sys, logger) +func newDatabaseWrapper(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) { + newDB, err := v5.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger) if err == nil { dbw = databaseVersionWrapper{ v5: newDB, @@ -32,7 +32,7 @@ func newDatabaseWrapper(ctx context.Context, pluginName string, sys pluginutil.L merr := &multierror.Error{} merr = multierror.Append(merr, err) - legacyDB, err := v4.PluginFactory(ctx, pluginName, sys, logger) + legacyDB, err := v4.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger) if err == nil { dbw = databaseVersionWrapper{ v4: legacyDB, diff --git a/builtin/logical/database/versioning_large_test.go b/builtin/logical/database/versioning_large_test.go index 0121327367..a9f7efde62 100644 --- a/builtin/logical/database/versioning_large_test.go +++ b/builtin/logical/database/versioning_large_test.go @@ -22,9 +22,9 @@ func TestPlugin_lifecycle(t *testing.T) { cluster, sys := getCluster(t) defer cluster.Cleanup() - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV4", []string{}, "") - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV5", []string{}, "") - vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "") + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV4", []string{}, "") + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "") + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "") config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} @@ -218,6 +218,216 @@ func TestPlugin_lifecycle(t *testing.T) { } } +func TestPlugin_VersionSelection(t *testing.T) { + cluster, sys := getCluster(t) + defer cluster.Cleanup() + + for _, version := range []string{"v11.0.0", "v11.0.1-rc1", "v2.0.0"} { + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, version, "TestBackend_PluginMain_MockV5", []string{}, "") + } + + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + config.System = sys + lb, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + b, ok := lb.(*databaseBackend) + if !ok { + t.Fatal("could not convert to database backend") + } + defer b.Cleanup(context.Background()) + + test := func(t *testing.T, selectVersion, expectedVersion string) func(t *testing.T) { + return func(t *testing.T) { + req := &logical.Request{ + Operation: logical.CreateOperation, + Path: "config/db", + Storage: config.StorageView, + Data: map[string]interface{}{ + "connection_url": "sample_connection_url", + "plugin_name": "mock-v5-database-plugin", + "plugin_version": selectVersion, + "verify_connection": true, + "allowed_roles": []string{"*"}, + "name": "mockv5", + "username": "mockv5-user", + "password": "mysecurepassword", + }, + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := b.HandleRequest(ctx, req) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + assertNoRespData(t, resp) + + defer func() { + _, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.DeleteOperation, + Path: "config/db", + Storage: config.StorageView, + }) + if err != nil { + t.Fatal(err) + } + }() + + req = &logical.Request{ + Operation: logical.ReadOperation, + Path: "config/db", + Storage: config.StorageView, + } + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err = b.HandleRequest(ctx, req) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + if resp.Data["plugin_version"].(string) != expectedVersion { + t.Fatalf("Expected version %q but got %q", expectedVersion, resp.Data["plugin_version"].(string)) + } + } + } + + for name, tc := range map[string]struct { + selectVersion string + expectedVersion string + }{ + "no version specified, selects latest in the absence of unversioned plugins": { + selectVersion: "", + expectedVersion: "v11.0.1-rc1", + }, + "specific version selected": { + selectVersion: "11.0.0", + expectedVersion: "v11.0.0", + }, + } { + t.Run(name, test(t, tc.selectVersion, tc.expectedVersion)) + } + + // Register a newer version of the plugin, and ensure that's the new default version selected. + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "v11.0.1", "TestBackend_PluginMain_MockV5", []string{}, "") + t.Run("no version specified, new latest version selected", test(t, "", "v11.0.1")) + + // Register an unversioned plugin and ensure that is now selected when no version is specified. + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "", "TestBackend_PluginMain_MockV5", []string{}, "") + for name, tc := range map[string]struct { + selectVersion string + expectedVersion string + }{ + "no version specified, selects unversioned": { + selectVersion: "", + expectedVersion: "", + }, + "specific version selected": { + selectVersion: "v2.0.0", + expectedVersion: "v2.0.0", + }, + } { + t.Run(name, test(t, tc.selectVersion, tc.expectedVersion)) + } +} + +func TestPlugin_VersionMustBeExplicitlyUpgraded(t *testing.T) { + cluster, sys := getCluster(t) + defer cluster.Cleanup() + + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + config.System = sys + lb, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + b, ok := lb.(*databaseBackend) + if !ok { + t.Fatal("could not convert to database backend") + } + defer b.Cleanup(context.Background()) + + configData := func(extraData ...string) map[string]interface{} { + data := map[string]interface{}{ + "connection_url": "sample_connection_url", + "plugin_name": "mysql-database-plugin", + "verify_connection": false, + "allowed_roles": []string{"*"}, + "username": "mockv5-user", + "password": "mysecurepassword", + } + if len(extraData)%2 != 0 { + t.Fatal("Expected an even number of args in extraData") + } + for i := 0; i < len(extraData); i += 2 { + data[extraData[i]] = extraData[i+1] + } + return data + } + + readVersion := func() string { + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "config/db", + Storage: config.StorageView, + }) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + return resp.Data["plugin_version"].(string) + } + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.CreateOperation, + Path: "config/db", + Storage: config.StorageView, + Data: configData(), + }) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + assertNoRespData(t, resp) + + version := readVersion() + expectedVersion := "" + if version != expectedVersion { + t.Fatalf("Expected version %q but got %q", expectedVersion, version) + } + + // Register versioned plugin, and check that a new write to existing config doesn't upgrade the plugin implicitly. + vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mysql-database-plugin", consts.PluginTypeDatabase, "v1.0.0", "TestBackend_PluginMain_MockV5", []string{}, "") + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/db", + Storage: config.StorageView, + Data: configData(), + }) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + assertNoRespData(t, resp) + + version = readVersion() + if version != expectedVersion { + t.Fatalf("Expected version %q but got %q", expectedVersion, version) + } + + // Now explicitly upgrade. + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/db", + Storage: config.StorageView, + Data: configData("plugin_version", "1.0.0"), + }) + assertErrIsNil(t, err) + assertRespHasNoErr(t, resp) + assertNoRespData(t, resp) + + version = readVersion() + expectedVersion = "v1.0.0" + if version != expectedVersion { + t.Fatalf("Expected version %q but got %q", expectedVersion, version) + } +} + func cleanup(t *testing.T, b *databaseBackend, reqs []*logical.Request) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/builtin/plugin/backend_lazyLoad_test.go b/builtin/plugin/backend_lazyLoad_test.go index 3d870a7a42..4d2727037a 100644 --- a/builtin/plugin/backend_lazyLoad_test.go +++ b/builtin/plugin/backend_lazyLoad_test.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "errors" "testing" "github.com/hashicorp/vault/sdk/helper/logging" @@ -193,3 +194,7 @@ func (v testSystemView) LookupPluginVersion(context.Context, string, consts.Plug }, }, nil } + +func (v testSystemView) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) { + return nil, errors.New("ListVersionedPlugins not implemented for testSystemView") +} diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go index ef05f748bf..27533a5827 100644 --- a/builtin/plugin/backend_test.go +++ b/builtin/plugin/backend_test.go @@ -133,7 +133,7 @@ func testConfig(t *testing.T, pluginCmd string) (*logical.BackendConfig, func()) os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, pluginCmd, []string{}, "") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", pluginCmd, []string{}, "") return config, func() { cluster.Cleanup() diff --git a/http/plugin_test.go b/http/plugin_test.go index dcb5d436d2..164a3d25f6 100644 --- a/http/plugin_test.go +++ b/http/plugin_test.go @@ -52,7 +52,7 @@ func getPluginClusterAndCore(t testing.TB, logger log.Logger) (*vault.TestCluste os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) vault.TestWaitActive(benchhelpers.TBtoT(t), core.Core) - vault.TestAddTestPlugin(benchhelpers.TBtoT(t), core.Core, "mock-plugin", consts.PluginTypeSecrets, "TestPlugin_PluginMain", []string{}, "") + vault.TestAddTestPlugin(benchhelpers.TBtoT(t), core.Core, "mock-plugin", consts.PluginTypeSecrets, "", "TestPlugin_PluginMain", []string{}, "") // Mount the mock plugin err = core.Client.Sys().Mount("mock", &api.MountInput{ diff --git a/sdk/database/dbplugin/plugin.go b/sdk/database/dbplugin/plugin.go index 6788e3379d..29f2f1f898 100644 --- a/sdk/database/dbplugin/plugin.go +++ b/sdk/database/dbplugin/plugin.go @@ -71,9 +71,15 @@ type Database interface { // PluginFactory is used to build plugin database types. It wraps the database // object in a logging and metrics middleware. -func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { +func PluginFactory(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { + return PluginFactoryVersion(ctx, pluginName, "", sys, logger) +} + +// PluginFactory is used to build plugin database types with a version specified. +// It wraps the database object in a logging and metrics middleware. +func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { // Look for plugin in the plugin catalog - pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase) + pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, consts.PluginTypeDatabase, pluginVersion) if err != nil { return nil, err } diff --git a/sdk/database/dbplugin/v5/plugin_factory.go b/sdk/database/dbplugin/v5/plugin_factory.go index b87dc3a75a..f68cc5621a 100644 --- a/sdk/database/dbplugin/v5/plugin_factory.go +++ b/sdk/database/dbplugin/v5/plugin_factory.go @@ -13,8 +13,14 @@ import ( // PluginFactory is used to build plugin database types. It wraps the database // object in a logging and metrics middleware. func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { + return PluginFactoryVersion(ctx, pluginName, "", sys, logger) +} + +// PluginFactoryVersion is used to build plugin database types with a version specified. +// It wraps the database object in a logging and metrics middleware. +func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { // Look for plugin in the plugin catalog - pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase) + pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, consts.PluginTypeDatabase, pluginVersion) if err != nil { return nil, err } @@ -43,6 +49,7 @@ func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRu config := pluginutil.PluginClientConfig{ Name: pluginName, PluginType: consts.PluginTypeDatabase, + Version: pluginVersion, PluginSets: PluginSets, HandshakeConfig: HandshakeConfig, Logger: namedLogger, diff --git a/sdk/logical/system_view.go b/sdk/logical/system_view.go index 5e896b89e5..4e5627b1c8 100644 --- a/sdk/logical/system_view.go +++ b/sdk/logical/system_view.go @@ -60,6 +60,10 @@ type SystemView interface { // name and version. Returns a PluginRunner or an error if a plugin can not be found. LookupPluginVersion(ctx context.Context, pluginName string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) + // ListVersionedPlugins returns information about all plugins of a certain + // type in the catalog, including any versioning information stored for them. + ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) + // NewPluginClient returns a client for managing the lifecycle of plugin // processes NewPluginClient(ctx context.Context, config pluginutil.PluginClientConfig) (pluginutil.PluginClient, error) @@ -176,6 +180,10 @@ func (d StaticSystemView) LookupPluginVersion(_ context.Context, _ string, _ con return nil, errors.New("LookupPluginVersion is not implemented in StaticSystemView") } +func (d StaticSystemView) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) { + return nil, errors.New("ListVersionedPlugins is not implemented in StaticSystemView") +} + func (d StaticSystemView) MlockEnabled() bool { return d.EnableMlock } diff --git a/sdk/plugin/grpc_system.go b/sdk/plugin/grpc_system.go index 58647e4a60..3761269c48 100644 --- a/sdk/plugin/grpc_system.go +++ b/sdk/plugin/grpc_system.go @@ -111,6 +111,10 @@ func (s *gRPCSystemViewClient) LookupPluginVersion(_ context.Context, _ string, return nil, fmt.Errorf("cannot call LookupPluginVersion from a plugin backend") } +func (s *gRPCSystemViewClient) ListVersionedPlugins(_ context.Context, _ consts.PluginType) ([]pluginutil.VersionedPlugin, error) { + return nil, fmt.Errorf("cannot call ListVersionedPlugins from a plugin backend") +} + func (s *gRPCSystemViewClient) MlockEnabled() bool { reply, err := s.client.MlockEnabled(context.Background(), &pb.Empty{}) if err != nil { diff --git a/vault/auth.go b/vault/auth.go index 1211413710..feaff927c2 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -916,7 +916,11 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV return nil, err } if plug == nil { - return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, t) + errContext := t + if entry.Version != "" { + errContext += fmt.Sprintf(", version=%s", entry.Version) + } + return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext) } f = plugin.Factory diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 80663385e5..77eccf0876 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -251,12 +251,28 @@ func (d dynamicSystemView) LookupPluginVersion(ctx context.Context, name string, return nil, err } if r == nil { - return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, name) + errContext := name + if version != "" { + errContext += fmt.Sprintf(", version=%s", version) + } + return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext) } return r, nil } +// ListVersionedPlugins returns information about all plugins of a certain +// typein the catalog, including any versioning information stored for them. +func (d dynamicSystemView) ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) { + if d.core == nil { + return nil, fmt.Errorf("system view core is nil") + } + if d.core.pluginCatalog == nil { + return nil, fmt.Errorf("system view core plugin catalog is nil") + } + return d.core.pluginCatalog.ListVersionedPlugins(ctx, pluginType) +} + // MlockEnabled returns the configuration setting for enabling mlock on plugins. func (d dynamicSystemView) MlockEnabled() bool { return d.core.enableMlock diff --git a/vault/logical_system_integ_test.go b/vault/logical_system_integ_test.go index e573e431e6..1f2e729139 100644 --- a/vault/logical_system_integ_test.go +++ b/vault/logical_system_integ_test.go @@ -242,7 +242,7 @@ func TestSystemBackend_Plugin_MismatchType(t *testing.T) { core := cluster.Cores[0] // Add a credential backend with the same name - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "TestBackend_PluginMainCredentials", []string{}, "") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", "TestBackend_PluginMainCredentials", []string{}, "") // Make a request to lazy load the now-credential plugin // and expect an error @@ -335,13 +335,13 @@ 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", consts.PluginTypeSecrets, logicalVersionMap[tc.pluginVersion], []string{}, "") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", logicalVersionMap[tc.pluginVersion], []string{}, "") _, err = core.Client.Logical().Write("sys/mounts/mock-0", map[string]interface{}{ "type": "test", }) case logical.TypeCredential: // Add plugin back to the catalog - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, credentialVersionMap[tc.pluginVersion], []string{}, "") + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", credentialVersionMap[tc.pluginVersion], []string{}, "") _, err = core.Client.Logical().Write("sys/auth/mock-0", map[string]interface{}{ "type": "test", }) @@ -463,10 +463,10 @@ func testPlugin_continueOnError(t *testing.T, btype logical.BackendType, mismatc switch btype { case logical.TypeLogical: plugin := logicalVersionMap[tc.pluginVersion] - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, plugin, []string{}, cluster.TempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, []string{}, cluster.TempDir) case logical.TypeCredential: plugin := credentialVersionMap[tc.pluginVersion] - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, plugin, []string{}, cluster.TempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", plugin, []string{}, cluster.TempDir) } // Reload the plugin @@ -755,7 +755,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo switch backendType { case logical.TypeLogical: plugin := logicalVersionMap[pluginVersion] - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, plugin, []string{}, tempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, []string{}, tempDir) for i := 0; i < numMounts; i++ { // Alternate input styles for plugin_name on every other mount options := map[string]interface{}{ @@ -771,7 +771,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo } case logical.TypeCredential: plugin := credentialVersionMap[pluginVersion] - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, plugin, []string{}, tempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeCredential, "", plugin, []string{}, tempDir) for i := 0; i < numMounts; i++ { // Alternate input styles for plugin_name on every other mount options := map[string]interface{}{ @@ -826,7 +826,7 @@ func testSystemBackend_SingleCluster_Env(t *testing.T, env []string) *vault.Test os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "TestBackend_PluginMainEnv", env, tempDir) + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", "TestBackend_PluginMainEnv", env, tempDir) options := map[string]interface{}{ "type": "mock-plugin", } diff --git a/vault/mount.go b/vault/mount.go index 64c46b0dbc..de60353fd2 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -306,7 +306,7 @@ const mountStateUnmounting = "unmounting" type MountEntry struct { Table string `json:"table"` // The table it belongs to Path string `json:"path"` // Mount Path - Type string `json:"type"` // Logical backend Type + Type string `json:"type"` // Logical backend Type. NB: This is the plugin name, e.g. my-vault-plugin, NOT plugin type (e.g. auth). Description string `json:"description"` // User-provided description UUID string `json:"uuid"` // Barrier view UUID BackendAwareUUID string `json:"backend_aware_uuid"` // UUID that can be used by the backend as a helper when a consistent value is needed outside of storage. @@ -330,9 +330,9 @@ type MountEntry struct { synthesizedConfigCache sync.Map // version info - Version string `json:"version,omitempty"` - Sha string `json:"sha,omitempty"` - RunningVersion string `json:"running_version,omitempty"` + Version string `json:"version,omitempty"` // The semantic version of the mounted plugin, e.g. v1.2.3. + Sha string `json:"sha,omitempty"` // The SHA256 sum of the plugin binary. + RunningVersion string `json:"running_version,omitempty"` // The semantic version of the mounted plugin as reported by the plugin. RunningSha string `json:"running_sha,omitempty"` } @@ -1526,7 +1526,11 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView return nil, err } if plug == nil { - return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, t) + errContext := t + if entry.Version != "" { + errContext += fmt.Sprintf(", version=%s", entry.Version) + } + return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext) } f = plugin.Factory @@ -1548,6 +1552,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView } conf["plugin_type"] = consts.PluginTypeSecrets.String() + conf["plugin_version"] = entry.Version backendLogger := c.baseLogger.Named(fmt.Sprintf("secrets.%s.%s", t, entry.Accessor)) c.AddLogger(backendLogger) diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index e2a16d8b10..f3332a70ed 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -373,13 +373,13 @@ func TestPluginCatalog_NewPluginClient(t *testing.T) { } // register plugins - TestAddTestPlugin(t, core, "mux-postgres", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_PostgresMultiplexed", []string{}, "") - TestAddTestPlugin(t, core, "single-postgres-1", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Postgres", []string{}, "") - TestAddTestPlugin(t, core, "single-postgres-2", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Postgres", []string{}, "") + TestAddTestPlugin(t, core, "mux-postgres", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_PostgresMultiplexed", []string{}, "") + TestAddTestPlugin(t, core, "single-postgres-1", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Postgres", []string{}, "") + TestAddTestPlugin(t, core, "single-postgres-2", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Postgres", []string{}, "") - TestAddTestPlugin(t, core, "mux-userpass", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_UserpassMultiplexed", []string{}, "") - TestAddTestPlugin(t, core, "single-userpass-1", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Userpass", []string{}, "") - TestAddTestPlugin(t, core, "single-userpass-2", consts.PluginTypeUnknown, "TestPluginCatalog_PluginMain_Userpass", []string{}, "") + TestAddTestPlugin(t, core, "mux-userpass", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_UserpassMultiplexed", []string{}, "") + TestAddTestPlugin(t, core, "single-userpass-1", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Userpass", []string{}, "") + TestAddTestPlugin(t, core, "single-userpass-2", consts.PluginTypeUnknown, "", "TestPluginCatalog_PluginMain_Userpass", []string{}, "") var pluginClients []*pluginClient // run plugins diff --git a/vault/testing.go b/vault/testing.go index fea43e699f..ecafe5e221 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -513,7 +513,7 @@ func TestDynamicSystemView(c *Core, ns *namespace.Namespace) *dynamicSystemView // TestAddTestPlugin registers the testFunc as part of the plugin command to the // plugin catalog. If provided, uses tmpDir as the plugin directory. -func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.PluginType, testFunc string, env []string, tempDir string) { +func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.PluginType, version string, testFunc string, env []string, tempDir string) { file, err := os.Open(os.Args[0]) if err != nil { t.Fatal(err) @@ -575,7 +575,6 @@ func TestAddTestPlugin(t testing.T, c *Core, name string, pluginType consts.Plug c.pluginCatalog.directory = fullPath args := []string{fmt.Sprintf("--test.run=%s", testFunc)} - version := "" err = c.pluginCatalog.Set(context.Background(), name, pluginType, version, fileName, args, env, sum) if err != nil { t.Fatal(err)