From dcd0677d459c57b150de62269a1f2d6caee6f359 Mon Sep 17 00:00:00 2001 From: Michael Golowka <72365+pcman312@users.noreply.github.com> Date: Fri, 7 Aug 2020 16:26:43 -0600 Subject: [PATCH] Add DBv5 plugin serving & management functions This mirrors what DBv4 is doing, but with the updated interface --- .../newdbplugin/grpc_database_plugin.go | 42 ++++++++++ sdk/database/newdbplugin/plugin_client.go | 80 +++++++++++++++++++ sdk/database/newdbplugin/plugin_factory.go | 78 ++++++++++++++++++ sdk/database/newdbplugin/plugin_server.go | 42 ++++++++++ 4 files changed, 242 insertions(+) create mode 100644 sdk/database/newdbplugin/grpc_database_plugin.go create mode 100644 sdk/database/newdbplugin/plugin_client.go create mode 100644 sdk/database/newdbplugin/plugin_factory.go create mode 100644 sdk/database/newdbplugin/plugin_server.go diff --git a/sdk/database/newdbplugin/grpc_database_plugin.go b/sdk/database/newdbplugin/grpc_database_plugin.go new file mode 100644 index 000000000000..66468640ce58 --- /dev/null +++ b/sdk/database/newdbplugin/grpc_database_plugin.go @@ -0,0 +1,42 @@ +package newdbplugin + +import ( + "context" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/sdk/database/newdbplugin/proto" + "google.golang.org/grpc" +) + +// handshakeConfigs are used to just do a basic handshake between +// a plugin and host. If the handshake fails, a user friendly error is shown. +// This prevents users from executing bad plugins or executing a plugin +// directory. It is a UX feature, not a security feature. +var handshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 5, + MagicCookieKey: "VAULT_DATABASE_PLUGIN", + MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb", +} + +type GRPCDatabasePlugin struct { + Impl Database + + // Embeding this will disable the netRPC protocol + plugin.NetRPCUnsupportedPlugin +} + +var _ plugin.Plugin = &GRPCDatabasePlugin{} +var _ plugin.GRPCPlugin = &GRPCDatabasePlugin{} + +func (d GRPCDatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterDatabaseServer(s, gRPCServer{impl: d.Impl}) + return nil +} + +func (GRPCDatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + client := gRPCClient{ + client: proto.NewDatabaseClient(c), + doneCtx: doneCtx, + } + return client, nil +} diff --git a/sdk/database/newdbplugin/plugin_client.go b/sdk/database/newdbplugin/plugin_client.go new file mode 100644 index 000000000000..812adc2b57f9 --- /dev/null +++ b/sdk/database/newdbplugin/plugin_client.go @@ -0,0 +1,80 @@ +package newdbplugin + +import ( + "context" + "errors" + "sync" + + log "github.com/hashicorp/go-hclog" + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/sdk/helper/pluginutil" +) + +// DatabasePluginClient embeds a databasePluginRPCClient and wraps it's Close +// method to also call Kill() on the plugin.Client. +type DatabasePluginClient struct { + client *plugin.Client + sync.Mutex + + Database +} + +// This wraps the Close call and ensures we both close the database connection +// and kill the plugin. +func (dc *DatabasePluginClient) Close() error { + err := dc.Database.Close() + dc.client.Kill() + + return err +} + +// NewPluginClient returns a databaseRPCClient with a connection to a running +// plugin. The client is wrapped in a DatabasePluginClient object to ensure the +// plugin is killed on call of Close(). +func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger, isMetadataMode bool) (Database, error) { + // pluginSets is the map of plugins we can dispense. + pluginSets := map[int]plugin.PluginSet{ + 5: plugin.PluginSet{ + "database": new(GRPCDatabasePlugin), + }, + } + + var client *plugin.Client + var err error + if isMetadataMode { + client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginSets, handshakeConfig, []string{}, logger) + } else { + client, err = pluginRunner.Run(ctx, sys, pluginSets, handshakeConfig, []string{}, logger) + } + if err != nil { + return nil, err + } + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + // Request the plugin + raw, err := rpcClient.Dispense("database") + if err != nil { + return nil, err + } + + // We should have a database type now. This feels like a normal interface + // implementation but is in fact over an RPC connection. + var db Database + switch raw.(type) { + case gRPCClient: + db = raw.(gRPCClient) + default: + return nil, errors.New("unsupported client type") + } + + // Wrap RPC implementation in DatabasePluginClient + return &DatabasePluginClient{ + client: client, + Database: db, + }, nil +} diff --git a/sdk/database/newdbplugin/plugin_factory.go b/sdk/database/newdbplugin/plugin_factory.go new file mode 100644 index 000000000000..eb8fd15d759f --- /dev/null +++ b/sdk/database/newdbplugin/plugin_factory.go @@ -0,0 +1,78 @@ +package newdbplugin + +import ( + "context" + "fmt" + + "github.com/hashicorp/errwrap" + log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/helper/pluginutil" +) + +// 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) { + // Look for plugin in the plugin catalog + pluginRunner, err := sys.LookupPlugin(ctx, pluginName, consts.PluginTypeDatabase) + if err != nil { + return nil, err + } + + namedLogger := logger.Named(pluginName) + + var transport string + var db Database + if pluginRunner.Builtin { + // Plugin is builtin so we can retrieve an instance of the interface + // from the pluginRunner. Then cast it to a Database. + dbRaw, err := pluginRunner.BuiltinFactory() + if err != nil { + return nil, errwrap.Wrapf("error initializing plugin: {{err}}", err) + } + + var ok bool + db, ok = dbRaw.(Database) + if !ok { + return nil, fmt.Errorf("unsupported database type: %q", pluginName) + } + + transport = "builtin" + + } else { + // create a DatabasePluginClient instance + db, err = NewPluginClient(ctx, sys, pluginRunner, namedLogger, false) + if err != nil { + return nil, err + } + + // Switch on the underlying database client type to get the transport + // method. + switch db.(*DatabasePluginClient).Database.(type) { + case *gRPCClient: + transport = "gRPC" + } + + } + + typeStr, err := db.Type() + if err != nil { + return nil, errwrap.Wrapf("error getting plugin type: {{err}}", err) + } + + // Wrap with metrics middleware + db = &databaseMetricsMiddleware{ + next: db, + typeStr: typeStr, + } + + // Wrap with tracing middleware + if namedLogger.IsTrace() { + db = &databaseTracingMiddleware{ + next: db, + logger: namedLogger.With("transport", transport), + } + } + + return db, nil +} diff --git a/sdk/database/newdbplugin/plugin_server.go b/sdk/database/newdbplugin/plugin_server.go new file mode 100644 index 000000000000..ccb178c8746e --- /dev/null +++ b/sdk/database/newdbplugin/plugin_server.go @@ -0,0 +1,42 @@ +package newdbplugin + +import ( + "crypto/tls" + "fmt" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/sdk/helper/pluginutil" +) + +// Serve is called from within a plugin and wraps the provided +// Database implementation in a databasePluginRPCServer object and starts a +// RPC server. +func Serve(db Database, tlsProvider func() (*tls.Config, error)) { + plugin.Serve(ServeConfig(db, tlsProvider)) +} + +func ServeConfig(db Database, tlsProvider func() (*tls.Config, error)) *plugin.ServeConfig { + err := pluginutil.OptionallyEnableMlock() + if err != nil { + fmt.Println(err) + return nil + } + + // pluginSets is the map of plugins we can dispense. + pluginSets := map[int]plugin.PluginSet{ + 5: plugin.PluginSet{ + "database": &GRPCDatabasePlugin{ + Impl: db, + }, + }, + } + + conf := &plugin.ServeConfig{ + HandshakeConfig: handshakeConfig, + VersionedPlugins: pluginSets, + TLSProvider: tlsProvider, + GRPCServer: plugin.DefaultGRPCServer, + } + + return conf +}