342b61984a
Move version out of SDK. For now it's a copy rather than move: the part not addressed by this change is sdk/helper/useragent.String, which we'll want to remove in favour of PluginString. That will have to wait until we've removed uses of useragent.String from all builtins.
1112 lines
35 KiB
Go
1112 lines
35 KiB
Go
package vault
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-plugin"
|
|
"github.com/hashicorp/go-secure-stdlib/base62"
|
|
semver "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/vault/helper/versions"
|
|
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
|
|
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
backendplugin "github.com/hashicorp/vault/sdk/plugin"
|
|
"github.com/hashicorp/vault/version"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
var (
|
|
pluginCatalogPath = "core/plugin-catalog/"
|
|
ErrDirectoryNotConfigured = errors.New("could not set plugin, plugin directory is not configured")
|
|
ErrPluginNotFound = errors.New("plugin not found in the catalog")
|
|
ErrPluginConnectionNotFound = errors.New("plugin connection not found for client")
|
|
ErrPluginBadType = errors.New("unable to determine plugin type")
|
|
)
|
|
|
|
// PluginCatalog keeps a record of plugins known to vault. External plugins need
|
|
// to be registered to the catalog before they can be used in backends. Builtin
|
|
// plugins are automatically detected and included in the catalog.
|
|
type PluginCatalog struct {
|
|
builtinRegistry BuiltinRegistry
|
|
catalogView *BarrierView
|
|
directory string
|
|
logger log.Logger
|
|
|
|
// externalPlugins holds plugin process connections by a key which is
|
|
// generated from the plugin runner config.
|
|
//
|
|
// This allows plugins that suppport multiplexing to use a single grpc
|
|
// connection to communicate with multiple "backends". Each backend
|
|
// configuration using the same plugin will be routed to the existing
|
|
// plugin process.
|
|
externalPlugins map[externalPluginsKey]*externalPlugin
|
|
mlockPlugins bool
|
|
|
|
lock sync.RWMutex
|
|
wrapper pluginutil.RunnerUtil
|
|
}
|
|
|
|
// Only plugins running with identical PluginRunner config can be multiplexed,
|
|
// so we use the PluginRunner input as the key for the external plugins map.
|
|
//
|
|
// However, to be a map key, it must be comparable:
|
|
// https://go.dev/ref/spec#Comparison_operators.
|
|
// In particular, the PluginRunner struct has slices and a function which are not
|
|
// comparable, so we need to transform it into a struct which is.
|
|
type externalPluginsKey struct {
|
|
name string
|
|
typ consts.PluginType
|
|
version string
|
|
command string
|
|
args string
|
|
env string
|
|
sha256 string
|
|
builtin bool
|
|
}
|
|
|
|
func makeExternalPluginsKey(p *pluginutil.PluginRunner) (externalPluginsKey, error) {
|
|
args, err := json.Marshal(p.Args)
|
|
if err != nil {
|
|
return externalPluginsKey{}, err
|
|
}
|
|
|
|
env, err := json.Marshal(p.Env)
|
|
if err != nil {
|
|
return externalPluginsKey{}, err
|
|
}
|
|
|
|
return externalPluginsKey{
|
|
name: p.Name,
|
|
typ: p.Type,
|
|
version: p.Version,
|
|
command: p.Command,
|
|
args: string(args),
|
|
env: string(env),
|
|
sha256: hex.EncodeToString(p.Sha256),
|
|
builtin: p.Builtin,
|
|
}, nil
|
|
}
|
|
|
|
// externalPlugin holds client connections for multiplexed and
|
|
// non-multiplexed plugin processes
|
|
type externalPlugin struct {
|
|
// connections holds client connections by ID
|
|
connections map[string]*pluginClient
|
|
|
|
multiplexingSupport bool
|
|
}
|
|
|
|
// pluginClient represents a connection to a plugin process
|
|
type pluginClient struct {
|
|
logger log.Logger
|
|
|
|
// id is the connection ID
|
|
id string
|
|
pid int
|
|
|
|
// client handles the lifecycle of a plugin process
|
|
// multiplexed plugins share the same client
|
|
client *plugin.Client
|
|
clientConn grpc.ClientConnInterface
|
|
cleanupFunc func() error
|
|
reloadFunc func() error
|
|
|
|
plugin.ClientProtocol
|
|
}
|
|
|
|
func wrapFactoryCheckPerms(core *Core, f logical.Factory) logical.Factory {
|
|
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
|
pluginName := conf.Config["plugin_name"]
|
|
pluginVersion := conf.Config["plugin_version"]
|
|
pluginTypeRaw := conf.Config["plugin_type"]
|
|
pluginType, err := consts.ParsePluginType(pluginTypeRaw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pluginDescription := fmt.Sprintf("%s plugin %s", pluginTypeRaw, pluginName)
|
|
if pluginVersion != "" {
|
|
pluginDescription += " version " + pluginVersion
|
|
}
|
|
|
|
plugin, err := core.pluginCatalog.Get(ctx, pluginName, pluginType, pluginVersion)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find %s in plugin catalog: %w", pluginDescription, err)
|
|
}
|
|
if plugin == nil {
|
|
return nil, fmt.Errorf("failed to find %s in plugin catalog", pluginDescription)
|
|
}
|
|
|
|
command, err := filepath.Rel(core.pluginCatalog.directory, plugin.Command)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compute plugin command: %w", err)
|
|
}
|
|
|
|
if err := core.CheckPluginPerms(command); err != nil {
|
|
return nil, err
|
|
}
|
|
return f(ctx, conf)
|
|
}
|
|
}
|
|
|
|
func (c *Core) setupPluginCatalog(ctx context.Context) error {
|
|
c.pluginCatalog = &PluginCatalog{
|
|
builtinRegistry: c.builtinRegistry,
|
|
catalogView: NewBarrierView(c.barrier, pluginCatalogPath),
|
|
directory: c.pluginDirectory,
|
|
logger: c.logger,
|
|
mlockPlugins: c.enableMlock,
|
|
wrapper: logical.StaticSystemView{VersionString: version.GetVersion().Version},
|
|
}
|
|
|
|
// Run upgrade if untyped plugins exist
|
|
err := c.pluginCatalog.UpgradePlugins(ctx, c.logger)
|
|
if err != nil {
|
|
c.logger.Error("error while upgrading plugin storage", "error", err)
|
|
return err
|
|
}
|
|
|
|
if c.logger.IsInfo() {
|
|
c.logger.Info("successfully setup plugin catalog", "plugin-directory", c.pluginDirectory)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type pluginClientConn struct {
|
|
*grpc.ClientConn
|
|
id string
|
|
}
|
|
|
|
var _ grpc.ClientConnInterface = &pluginClientConn{}
|
|
|
|
func (d *pluginClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
|
// Inject ID to the context
|
|
md := metadata.Pairs(pluginutil.MultiplexingCtxKey, d.id)
|
|
idCtx := metadata.NewOutgoingContext(ctx, md)
|
|
|
|
return d.ClientConn.Invoke(idCtx, method, args, reply, opts...)
|
|
}
|
|
|
|
func (d *pluginClientConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
// Inject ID to the context
|
|
md := metadata.Pairs(pluginutil.MultiplexingCtxKey, d.id)
|
|
idCtx := metadata.NewOutgoingContext(ctx, md)
|
|
|
|
return d.ClientConn.NewStream(idCtx, desc, method, opts...)
|
|
}
|
|
|
|
func (p *pluginClient) Conn() grpc.ClientConnInterface {
|
|
return p.clientConn
|
|
}
|
|
|
|
func (p *pluginClient) Reload() error {
|
|
p.logger.Debug("reload external plugin process")
|
|
return p.reloadFunc()
|
|
}
|
|
|
|
// reloadExternalPlugin
|
|
// This should be called with the write lock held.
|
|
func (c *PluginCatalog) reloadExternalPlugin(key externalPluginsKey, id, path string) error {
|
|
extPlugin, ok := c.externalPlugins[key]
|
|
if !ok {
|
|
return fmt.Errorf("plugin client not found")
|
|
}
|
|
if !extPlugin.multiplexingSupport {
|
|
err := c.cleanupExternalPlugin(key, id, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
pc, ok := extPlugin.connections[id]
|
|
if !ok {
|
|
return fmt.Errorf("%w id: %s", ErrPluginConnectionNotFound, id)
|
|
}
|
|
|
|
delete(c.externalPlugins, key)
|
|
pc.client.Kill()
|
|
c.logger.Debug("killed external plugin process for reload", "path", path, "pid", pc.pid)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close calls the plugin client's cleanupFunc to do any necessary cleanup on
|
|
// the plugin client and the PluginCatalog. This implements the
|
|
// plugin.ClientProtocol interface.
|
|
func (p *pluginClient) Close() error {
|
|
p.logger.Debug("cleaning up plugin client connection", "id", p.id)
|
|
return p.cleanupFunc()
|
|
}
|
|
|
|
// cleanupExternalPlugin will kill plugin processes and perform any necessary
|
|
// cleanup on the externalPlugins map for multiplexed and non-multiplexed
|
|
// plugins. This should be called with the write lock held.
|
|
func (c *PluginCatalog) cleanupExternalPlugin(key externalPluginsKey, id, path string) error {
|
|
extPlugin, ok := c.externalPlugins[key]
|
|
if !ok {
|
|
return fmt.Errorf("plugin client not found")
|
|
}
|
|
|
|
pc, ok := extPlugin.connections[id]
|
|
if !ok {
|
|
// this can happen if the backend is reloaded due to a plugin process
|
|
// being killed out of band
|
|
c.logger.Warn(ErrPluginConnectionNotFound.Error(), "id", id)
|
|
return fmt.Errorf("%w id: %s", ErrPluginConnectionNotFound, id)
|
|
}
|
|
|
|
delete(extPlugin.connections, id)
|
|
c.logger.Debug("removed plugin client connection", "id", id)
|
|
|
|
if !extPlugin.multiplexingSupport {
|
|
pc.client.Kill()
|
|
|
|
if len(extPlugin.connections) == 0 {
|
|
delete(c.externalPlugins, key)
|
|
}
|
|
c.logger.Debug("killed external plugin process", "path", path, "pid", pc.pid)
|
|
} else if len(extPlugin.connections) == 0 || pc.client.Exited() {
|
|
pc.client.Kill()
|
|
delete(c.externalPlugins, key)
|
|
c.logger.Debug("killed external multiplexed plugin process", "path", path, "pid", pc.pid)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *PluginCatalog) getExternalPlugin(key externalPluginsKey) *externalPlugin {
|
|
if extPlugin, ok := c.externalPlugins[key]; ok {
|
|
return extPlugin
|
|
}
|
|
|
|
return c.newExternalPlugin(key)
|
|
}
|
|
|
|
func (c *PluginCatalog) newExternalPlugin(key externalPluginsKey) *externalPlugin {
|
|
if c.externalPlugins == nil {
|
|
c.externalPlugins = make(map[externalPluginsKey]*externalPlugin)
|
|
}
|
|
|
|
extPlugin := &externalPlugin{
|
|
connections: make(map[string]*pluginClient),
|
|
}
|
|
|
|
c.externalPlugins[key] = extPlugin
|
|
return extPlugin
|
|
}
|
|
|
|
// NewPluginClient returns a client for managing the lifecycle of a plugin
|
|
// process
|
|
func (c *PluginCatalog) NewPluginClient(ctx context.Context, config pluginutil.PluginClientConfig) (*pluginClient, error) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
if config.Name == "" {
|
|
return nil, fmt.Errorf("no name provided for plugin")
|
|
}
|
|
if config.PluginType == consts.PluginTypeUnknown {
|
|
return nil, fmt.Errorf("no plugin type provided")
|
|
}
|
|
|
|
pluginRunner, err := c.get(ctx, config.Name, config.PluginType, config.Version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to lookup plugin: %w", err)
|
|
}
|
|
if pluginRunner == nil {
|
|
return nil, fmt.Errorf("no plugin found")
|
|
}
|
|
pc, err := c.newPluginClient(ctx, pluginRunner, config)
|
|
return pc, err
|
|
}
|
|
|
|
// newPluginClient returns a client for managing the lifecycle of a plugin
|
|
// process. Callers should have the write lock held.
|
|
func (c *PluginCatalog) newPluginClient(ctx context.Context, pluginRunner *pluginutil.PluginRunner, config pluginutil.PluginClientConfig) (*pluginClient, error) {
|
|
if pluginRunner == nil {
|
|
return nil, fmt.Errorf("no plugin found")
|
|
}
|
|
|
|
key, err := makeExternalPluginsKey(pluginRunner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
extPlugin := c.getExternalPlugin(key)
|
|
id, err := base62.Random(10)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pc := &pluginClient{
|
|
id: id,
|
|
logger: c.logger.Named(pluginRunner.Name),
|
|
cleanupFunc: func() error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
return c.cleanupExternalPlugin(key, id, pluginRunner.Command)
|
|
},
|
|
reloadFunc: func() error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
return c.reloadExternalPlugin(key, id, pluginRunner.Command)
|
|
},
|
|
}
|
|
|
|
// Multiplexing support will always be false initially, but will be
|
|
// adjusted once we query from the plugin whether it can multiplex or not
|
|
if !extPlugin.multiplexingSupport || len(extPlugin.connections) == 0 {
|
|
c.logger.Debug("spawning a new plugin process", "plugin_name", pluginRunner.Name, "id", id)
|
|
client, err := pluginRunner.RunConfig(ctx,
|
|
pluginutil.PluginSets(config.PluginSets),
|
|
pluginutil.HandshakeConfig(config.HandshakeConfig),
|
|
pluginutil.Logger(config.Logger),
|
|
pluginutil.MetadataMode(config.IsMetadataMode),
|
|
pluginutil.MLock(c.mlockPlugins),
|
|
pluginutil.AutoMTLS(config.AutoMTLS),
|
|
pluginutil.Runner(config.Wrapper),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pc.client = client
|
|
} else {
|
|
c.logger.Debug("returning existing plugin client for multiplexed plugin", "id", id)
|
|
|
|
// get the first client, since they are all the same
|
|
for k := range extPlugin.connections {
|
|
pc.client = extPlugin.connections[k].client
|
|
break
|
|
}
|
|
|
|
if pc.client == nil {
|
|
return nil, fmt.Errorf("plugin client is nil")
|
|
}
|
|
}
|
|
|
|
// Get the protocol client for this connection.
|
|
// Subsequent calls to this will return the same client.
|
|
rpcClient, err := pc.client.Client()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get the external plugin pid
|
|
conf := pc.client.ReattachConfig()
|
|
if conf != nil {
|
|
pc.pid = conf.Pid
|
|
}
|
|
|
|
clientConn := rpcClient.(*plugin.GRPCClient).Conn
|
|
|
|
muxed, err := pluginutil.MultiplexingSupported(ctx, clientConn, config.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pc.clientConn = &pluginClientConn{
|
|
ClientConn: clientConn,
|
|
id: id,
|
|
}
|
|
|
|
pc.ClientProtocol = rpcClient
|
|
|
|
extPlugin.connections[id] = pc
|
|
extPlugin.multiplexingSupport = muxed
|
|
|
|
return extPlugin.connections[id], nil
|
|
}
|
|
|
|
// getPluginTypeFromUnknown will attempt to run the plugin to determine the
|
|
// type. It will first attempt to run as a database plugin then a backend
|
|
// plugin.
|
|
func (c *PluginCatalog) getPluginTypeFromUnknown(ctx context.Context, plugin *pluginutil.PluginRunner) (consts.PluginType, error) {
|
|
merr := &multierror.Error{}
|
|
err := c.isDatabasePlugin(ctx, plugin)
|
|
if err == nil {
|
|
return consts.PluginTypeDatabase, nil
|
|
}
|
|
merr = multierror.Append(merr, err)
|
|
|
|
pluginType, err := c.getBackendPluginType(ctx, plugin)
|
|
if err == nil {
|
|
return pluginType, nil
|
|
}
|
|
merr = multierror.Append(merr, err)
|
|
|
|
return consts.PluginTypeUnknown, merr
|
|
}
|
|
|
|
// getBackendPluginType returns an error if the plugin is not a backend plugin.
|
|
func (c *PluginCatalog) getBackendPluginType(ctx context.Context, pluginRunner *pluginutil.PluginRunner) (consts.PluginType, error) {
|
|
merr := &multierror.Error{}
|
|
// Attempt to run as backend plugin
|
|
config := pluginutil.PluginClientConfig{
|
|
Name: pluginRunner.Name,
|
|
PluginSets: backendplugin.PluginSet,
|
|
HandshakeConfig: backendplugin.HandshakeConfig,
|
|
Logger: log.NewNullLogger(),
|
|
IsMetadataMode: false,
|
|
AutoMTLS: true,
|
|
Wrapper: c.wrapper,
|
|
}
|
|
|
|
var client logical.Backend
|
|
var attemptV4 bool
|
|
// First, attempt to run as backend V5 plugin
|
|
c.logger.Debug("attempting to load backend plugin", "name", pluginRunner.Name)
|
|
pc, err := c.newPluginClient(ctx, pluginRunner, config)
|
|
if err == nil {
|
|
// we spawned a subprocess, so make sure to clean it up
|
|
key, err := makeExternalPluginsKey(pluginRunner)
|
|
if err != nil {
|
|
return consts.PluginTypeUnknown, err
|
|
}
|
|
defer func() {
|
|
// Close the client and cleanup the plugin process
|
|
err = c.cleanupExternalPlugin(key, pc.id, pluginRunner.Command)
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
}()
|
|
|
|
// dispense the plugin so we can get its type
|
|
client, err = backendplugin.Dispense(pc.ClientProtocol, pc)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to dispense plugin as backend v5: %w", err))
|
|
c.logger.Debug("failed to dispense v5 backend plugin", "name", pluginRunner.Name)
|
|
attemptV4 = true
|
|
} else {
|
|
c.logger.Debug("successfully dispensed v5 backend plugin", "name", pluginRunner.Name)
|
|
}
|
|
} else {
|
|
attemptV4 = true
|
|
}
|
|
|
|
if attemptV4 {
|
|
c.logger.Debug("failed to dispense v5 backend plugin", "name", pluginRunner.Name)
|
|
config.AutoMTLS = false
|
|
config.IsMetadataMode = true
|
|
// attempt to run as a v4 backend plugin
|
|
client, err = backendplugin.NewPluginClient(ctx, c.wrapper, pluginRunner, log.NewNullLogger(), true)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to dispense v4 backend plugin: %w", err))
|
|
c.logger.Debug("failed to dispense v4 backend plugin", "name", pluginRunner.Name, "error", merr)
|
|
return consts.PluginTypeUnknown, merr.ErrorOrNil()
|
|
}
|
|
c.logger.Debug("successfully dispensed v4 backend plugin", "name", pluginRunner.Name)
|
|
defer client.Cleanup(ctx)
|
|
}
|
|
|
|
err = client.Setup(ctx, &logical.BackendConfig{})
|
|
if err != nil {
|
|
return consts.PluginTypeUnknown, err
|
|
}
|
|
backendType := client.Type()
|
|
|
|
switch backendType {
|
|
case logical.TypeCredential:
|
|
return consts.PluginTypeCredential, nil
|
|
case logical.TypeLogical:
|
|
return consts.PluginTypeSecrets, nil
|
|
}
|
|
|
|
if client == nil || client.Type() == logical.TypeUnknown {
|
|
c.logger.Warn("unknown plugin type",
|
|
"plugin name", pluginRunner.Name,
|
|
"error", merr.Error())
|
|
} else {
|
|
c.logger.Warn("unsupported plugin type",
|
|
"plugin name", pluginRunner.Name,
|
|
"plugin type", client.Type().String(),
|
|
"error", merr.Error())
|
|
}
|
|
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as backend plugin: %w", err))
|
|
|
|
return consts.PluginTypeUnknown, merr.ErrorOrNil()
|
|
}
|
|
|
|
// getBackendRunningVersion attempts to get the plugin version
|
|
func (c *PluginCatalog) getBackendRunningVersion(ctx context.Context, pluginRunner *pluginutil.PluginRunner) (logical.PluginVersion, error) {
|
|
merr := &multierror.Error{}
|
|
// Attempt to run as backend plugin
|
|
config := pluginutil.PluginClientConfig{
|
|
Name: pluginRunner.Name,
|
|
PluginSets: backendplugin.PluginSet,
|
|
HandshakeConfig: backendplugin.HandshakeConfig,
|
|
Logger: log.NewNullLogger(),
|
|
IsMetadataMode: false,
|
|
AutoMTLS: true,
|
|
Wrapper: c.wrapper,
|
|
}
|
|
|
|
var client logical.Backend
|
|
// First, attempt to run as backend V5 plugin
|
|
c.logger.Debug("attempting to load backend plugin", "name", pluginRunner.Name)
|
|
pc, err := c.newPluginClient(ctx, pluginRunner, config)
|
|
if err == nil {
|
|
// we spawned a subprocess, so make sure to clean it up
|
|
key, err := makeExternalPluginsKey(pluginRunner)
|
|
if err != nil {
|
|
return logical.EmptyPluginVersion, err
|
|
}
|
|
defer func() {
|
|
// Close the client and cleanup the plugin process
|
|
err = c.cleanupExternalPlugin(key, pc.id, pluginRunner.Command)
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
}()
|
|
|
|
// dispense the plugin so we can get its version
|
|
client, err = backendplugin.Dispense(pc.ClientProtocol, pc)
|
|
if err == nil {
|
|
c.logger.Debug("successfully dispensed v5 backend plugin", "name", pluginRunner.Name)
|
|
|
|
err = client.Setup(ctx, &logical.BackendConfig{})
|
|
if err != nil {
|
|
return logical.EmptyPluginVersion, nil
|
|
}
|
|
if versioner, ok := client.(logical.PluginVersioner); ok {
|
|
return versioner.PluginVersion(), nil
|
|
}
|
|
return logical.EmptyPluginVersion, nil
|
|
}
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to dispense plugin as backend v5: %w", err))
|
|
}
|
|
c.logger.Debug("failed to dispense v5 backend plugin", "name", pluginRunner.Name, "error", err)
|
|
config.AutoMTLS = false
|
|
config.IsMetadataMode = true
|
|
// attempt to run as a v4 backend plugin
|
|
client, err = backendplugin.NewPluginClient(ctx, c.wrapper, pluginRunner, log.NewNullLogger(), true)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to dispense v4 backend plugin: %w", err))
|
|
c.logger.Debug("failed to dispense v4 backend plugin", "name", pluginRunner.Name, "error", merr)
|
|
return logical.EmptyPluginVersion, merr.ErrorOrNil()
|
|
}
|
|
c.logger.Debug("successfully dispensed v4 backend plugin", "name", pluginRunner.Name)
|
|
defer client.Cleanup(ctx)
|
|
|
|
err = client.Setup(ctx, &logical.BackendConfig{})
|
|
if err != nil {
|
|
return logical.EmptyPluginVersion, err
|
|
}
|
|
if versioner, ok := client.(logical.PluginVersioner); ok {
|
|
return versioner.PluginVersion(), nil
|
|
}
|
|
return logical.EmptyPluginVersion, nil
|
|
}
|
|
|
|
// getDatabaseRunningVersion returns the version reported by a database plugin
|
|
func (c *PluginCatalog) getDatabaseRunningVersion(ctx context.Context, pluginRunner *pluginutil.PluginRunner) (logical.PluginVersion, error) {
|
|
merr := &multierror.Error{}
|
|
config := pluginutil.PluginClientConfig{
|
|
Name: pluginRunner.Name,
|
|
PluginSets: v5.PluginSets,
|
|
PluginType: consts.PluginTypeDatabase,
|
|
Version: pluginRunner.Version,
|
|
HandshakeConfig: v5.HandshakeConfig,
|
|
Logger: log.Default(),
|
|
IsMetadataMode: true,
|
|
AutoMTLS: true,
|
|
Wrapper: c.wrapper,
|
|
}
|
|
|
|
// Attempt to run as database V5+ multiplexed plugin
|
|
c.logger.Debug("attempting to load database plugin as v5", "name", pluginRunner.Name)
|
|
v5Client, err := c.newPluginClient(ctx, pluginRunner, config)
|
|
if err == nil {
|
|
key, err := makeExternalPluginsKey(pluginRunner)
|
|
if err != nil {
|
|
return logical.EmptyPluginVersion, err
|
|
}
|
|
defer func() {
|
|
// Close the client and cleanup the plugin process
|
|
err = c.cleanupExternalPlugin(key, v5Client.id, pluginRunner.Command)
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
}()
|
|
|
|
raw, err := v5Client.Dispense("database")
|
|
if err != nil {
|
|
return logical.EmptyPluginVersion, err
|
|
}
|
|
if versioner, ok := raw.(logical.PluginVersioner); ok {
|
|
return versioner.PluginVersion(), nil
|
|
}
|
|
return logical.EmptyPluginVersion, nil
|
|
}
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v5: %w", err))
|
|
|
|
c.logger.Debug("attempting to load database plugin as v4", "name", pluginRunner.Name)
|
|
v4Client, err := v4.NewPluginClient(ctx, c.wrapper, pluginRunner, log.NewNullLogger(), true)
|
|
if err == nil {
|
|
// Close the client and cleanup the plugin process
|
|
defer func() {
|
|
err = v4Client.Close()
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
}()
|
|
|
|
if versioner, ok := v4Client.(logical.PluginVersioner); ok {
|
|
return versioner.PluginVersion(), nil
|
|
}
|
|
|
|
return logical.EmptyPluginVersion, nil
|
|
}
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v4: %w", err))
|
|
return logical.EmptyPluginVersion, merr
|
|
}
|
|
|
|
// isDatabasePlugin returns an error if the plugin is not a database plugin.
|
|
func (c *PluginCatalog) isDatabasePlugin(ctx context.Context, pluginRunner *pluginutil.PluginRunner) error {
|
|
merr := &multierror.Error{}
|
|
config := pluginutil.PluginClientConfig{
|
|
Name: pluginRunner.Name,
|
|
PluginSets: v5.PluginSets,
|
|
PluginType: consts.PluginTypeDatabase,
|
|
Version: pluginRunner.Version,
|
|
HandshakeConfig: v5.HandshakeConfig,
|
|
Logger: log.NewNullLogger(),
|
|
IsMetadataMode: true,
|
|
AutoMTLS: true,
|
|
Wrapper: c.wrapper,
|
|
}
|
|
|
|
// Attempt to run as database V5+ multiplexed plugin
|
|
c.logger.Debug("attempting to load database plugin as v5", "name", pluginRunner.Name)
|
|
v5Client, err := c.newPluginClient(ctx, pluginRunner, config)
|
|
if err == nil {
|
|
// Close the client and cleanup the plugin process
|
|
key, err := makeExternalPluginsKey(pluginRunner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = c.cleanupExternalPlugin(key, v5Client.id, pluginRunner.Command)
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v5: %w", err))
|
|
|
|
c.logger.Debug("attempting to load database plugin as v4", "name", pluginRunner.Name)
|
|
v4Client, err := v4.NewPluginClient(ctx, c.wrapper, pluginRunner, log.NewNullLogger(), true)
|
|
if err == nil {
|
|
// Close the client and cleanup the plugin process
|
|
err = v4Client.Close()
|
|
if err != nil {
|
|
c.logger.Error("error closing plugin client", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v4: %w", err))
|
|
|
|
return merr.ErrorOrNil()
|
|
}
|
|
|
|
// UpdatePlugins will loop over all the plugins of unknown type and attempt to
|
|
// upgrade them to typed plugins
|
|
func (c *PluginCatalog) UpgradePlugins(ctx context.Context, logger log.Logger) error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// If the directory isn't set we can skip the upgrade attempt
|
|
if c.directory == "" {
|
|
return nil
|
|
}
|
|
|
|
// List plugins from old location
|
|
pluginsRaw, err := c.catalogView.List(ctx, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plugins := make([]string, 0, len(pluginsRaw))
|
|
for _, p := range pluginsRaw {
|
|
if !strings.HasSuffix(p, "/") {
|
|
plugins = append(plugins, p)
|
|
}
|
|
}
|
|
|
|
logger.Info("upgrading plugin information", "plugins", plugins)
|
|
|
|
var retErr error
|
|
for _, pluginName := range plugins {
|
|
pluginRaw, err := c.catalogView.Get(ctx, pluginName)
|
|
if err != nil {
|
|
retErr = multierror.Append(fmt.Errorf("failed to load plugin entry: %w", err))
|
|
continue
|
|
}
|
|
|
|
plugin := new(pluginutil.PluginRunner)
|
|
if err := jsonutil.DecodeJSON(pluginRaw.Value, plugin); err != nil {
|
|
retErr = multierror.Append(fmt.Errorf("failed to decode plugin entry: %w", err))
|
|
continue
|
|
}
|
|
|
|
// prepend the plugin directory to the command
|
|
cmdOld := plugin.Command
|
|
plugin.Command = filepath.Join(c.directory, plugin.Command)
|
|
|
|
// Upgrade the storage. At this point we don't know what type of plugin this is so pass in the unknown type.
|
|
runner, err := c.setInternal(ctx, pluginName, consts.PluginTypeUnknown, plugin.Version, cmdOld, plugin.Args, plugin.Env, plugin.Sha256)
|
|
if err != nil {
|
|
if errors.Is(err, ErrPluginBadType) {
|
|
retErr = multierror.Append(retErr, fmt.Errorf("could not upgrade plugin %s: plugin of unknown type", pluginName))
|
|
continue
|
|
}
|
|
|
|
retErr = multierror.Append(retErr, fmt.Errorf("could not upgrade plugin %s: %s", pluginName, err))
|
|
continue
|
|
}
|
|
|
|
err = c.catalogView.Delete(ctx, pluginName)
|
|
if err != nil {
|
|
logger.Error("could not remove plugin", "plugin", pluginName, "error", err)
|
|
}
|
|
|
|
logger.Info("upgraded plugin type", "plugin", pluginName, "type", runner.Type.String())
|
|
}
|
|
|
|
return retErr
|
|
}
|
|
|
|
// Get retrieves a plugin with the specified name from the catalog. It first
|
|
// looks for external plugins with this name and then looks for builtin plugins.
|
|
// It returns a PluginRunner or an error if no plugin was found.
|
|
func (c *PluginCatalog) Get(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) {
|
|
c.lock.RLock()
|
|
runner, err := c.get(ctx, name, pluginType, version)
|
|
c.lock.RUnlock()
|
|
return runner, err
|
|
}
|
|
|
|
func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) {
|
|
// If the directory isn't set only look for builtin plugins.
|
|
if c.directory != "" {
|
|
// Look for external plugins in the barrier
|
|
storageKey := path.Join(pluginType.String(), name)
|
|
if version != "" {
|
|
storageKey = path.Join(storageKey, version)
|
|
}
|
|
out, err := c.catalogView.Get(ctx, storageKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve plugin %q: %w", name, err)
|
|
}
|
|
if out == nil && version == "" {
|
|
// Also look for external plugins under what their name would have been if they
|
|
// were registered before plugin types existed.
|
|
out, err = c.catalogView.Get(ctx, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve plugin %q: %w", name, err)
|
|
}
|
|
}
|
|
if out != nil {
|
|
entry := new(pluginutil.PluginRunner)
|
|
if err := jsonutil.DecodeJSON(out.Value, entry); err != nil {
|
|
return nil, fmt.Errorf("failed to decode plugin entry: %w", err)
|
|
}
|
|
if entry.Type != pluginType && entry.Type != consts.PluginTypeUnknown {
|
|
return nil, nil
|
|
}
|
|
|
|
// prepend the plugin directory to the command
|
|
entry.Command = filepath.Join(c.directory, entry.Command)
|
|
|
|
return entry, nil
|
|
}
|
|
}
|
|
|
|
builtinVersion := versions.GetBuiltinVersion(pluginType, name)
|
|
if version == "" || version == builtinVersion {
|
|
if version == builtinVersion {
|
|
// Don't return the builtin if it's shadowed by an unversioned plugin.
|
|
unversioned, err := c.get(ctx, name, pluginType, "")
|
|
if err == nil && unversioned != nil && !unversioned.Builtin {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// Look for builtin plugins
|
|
if factory, ok := c.builtinRegistry.Get(name, pluginType); ok {
|
|
return &pluginutil.PluginRunner{
|
|
Name: name,
|
|
Type: pluginType,
|
|
Builtin: true,
|
|
BuiltinFactory: factory,
|
|
Version: builtinVersion,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// 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 string, pluginType consts.PluginType, version string, command string, args []string, env []string, sha256 []byte) error {
|
|
if c.directory == "" {
|
|
return ErrDirectoryNotConfigured
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(name, ".."):
|
|
fallthrough
|
|
case strings.Contains(command, ".."):
|
|
return consts.ErrPathContainsParentReferences
|
|
}
|
|
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
_, err := c.setInternal(ctx, name, pluginType, version, command, args, env, sha256)
|
|
return err
|
|
}
|
|
|
|
func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType consts.PluginType, version string, command string, args []string, env []string, sha256 []byte) (*pluginutil.PluginRunner, error) {
|
|
// Best effort check to make sure the command isn't breaking out of the
|
|
// configured plugin directory.
|
|
commandFull := filepath.Join(c.directory, command)
|
|
sym, err := filepath.EvalSymlinks(commandFull)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
|
}
|
|
symAbs, err := filepath.Abs(filepath.Dir(sym))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
|
}
|
|
|
|
if symAbs != c.directory {
|
|
return nil, errors.New("cannot execute files outside of configured plugin directory")
|
|
}
|
|
|
|
// entryTmp should only be used for the below type and version checks, it uses the
|
|
// full command instead of the relative command.
|
|
entryTmp := &pluginutil.PluginRunner{
|
|
Name: name,
|
|
Command: commandFull,
|
|
Args: args,
|
|
Env: env,
|
|
Sha256: sha256,
|
|
Builtin: false,
|
|
}
|
|
// If the plugin type is unknown, we want to attempt to determine the type
|
|
if pluginType == consts.PluginTypeUnknown {
|
|
pluginType, err = c.getPluginTypeFromUnknown(ctx, entryTmp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pluginType == consts.PluginTypeUnknown {
|
|
return nil, ErrPluginBadType
|
|
}
|
|
}
|
|
|
|
// getting the plugin version is best-effort, so errors are not fatal
|
|
runningVersion := logical.EmptyPluginVersion
|
|
var versionErr error
|
|
switch pluginType {
|
|
case consts.PluginTypeSecrets, consts.PluginTypeCredential:
|
|
runningVersion, versionErr = c.getBackendRunningVersion(ctx, entryTmp)
|
|
case consts.PluginTypeDatabase:
|
|
runningVersion, versionErr = c.getDatabaseRunningVersion(ctx, entryTmp)
|
|
default:
|
|
return nil, fmt.Errorf("unknown plugin type: %v", pluginType)
|
|
}
|
|
if versionErr != nil {
|
|
c.logger.Warn("Error determining plugin version", "error", versionErr)
|
|
} else if version != "" && runningVersion.Version != "" && version != runningVersion.Version {
|
|
c.logger.Warn("Plugin self-reported version did not match requested version", "plugin", name, "requestedVersion", version, "reportedVersion", runningVersion.Version)
|
|
return nil, fmt.Errorf("plugin version mismatch: %s reported version (%s) did not match requested version (%s)", name, runningVersion.Version, version)
|
|
} else if version == "" && runningVersion.Version != "" {
|
|
version = runningVersion.Version
|
|
_, err := semver.NewVersion(version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("plugin self-reported version %q is not a valid semantic version: %w", version, err)
|
|
}
|
|
|
|
}
|
|
|
|
entry := &pluginutil.PluginRunner{
|
|
Name: name,
|
|
Type: pluginType,
|
|
Version: version,
|
|
Command: command,
|
|
Args: args,
|
|
Env: env,
|
|
Sha256: sha256,
|
|
Builtin: false,
|
|
}
|
|
|
|
buf, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode plugin entry: %w", err)
|
|
}
|
|
|
|
storageKey := path.Join(pluginType.String(), name)
|
|
if version != "" {
|
|
storageKey = path.Join(storageKey, version)
|
|
}
|
|
logicalEntry := logical.StorageEntry{
|
|
Key: storageKey,
|
|
Value: buf,
|
|
}
|
|
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
|
|
return nil, fmt.Errorf("failed to persist plugin entry: %w", err)
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
// Delete is used to remove an external plugin from the catalog. Builtin plugins
|
|
// can not be deleted.
|
|
func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType, pluginVersion string) error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// Check the name under which the plugin exists, but if it's unfound, don't return any error.
|
|
pluginKey := path.Join(pluginType.String(), name)
|
|
if pluginVersion != "" {
|
|
pluginKey = path.Join(pluginKey, pluginVersion)
|
|
}
|
|
out, err := c.catalogView.Get(ctx, pluginKey)
|
|
if err != nil || out == nil {
|
|
pluginKey = name
|
|
}
|
|
|
|
return c.catalogView.Delete(ctx, pluginKey)
|
|
}
|
|
|
|
// List returns a list of all the known plugin names. If an external and builtin
|
|
// plugin share the same name, only one instance of the name will be returned.
|
|
func (c *PluginCatalog) List(ctx context.Context, pluginType consts.PluginType) ([]string, error) {
|
|
plugins, err := c.listInternal(ctx, pluginType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use a set to de-dupe between builtin and unversioned external plugins.
|
|
// External plugins with the same name as a builtin override the builtin.
|
|
uniquePluginNames := make(map[string]struct{})
|
|
for _, plugin := range plugins {
|
|
uniquePluginNames[plugin.Name] = struct{}{}
|
|
}
|
|
|
|
retList := make([]string, 0, len(uniquePluginNames))
|
|
for plugin := range uniquePluginNames {
|
|
retList = append(retList, plugin)
|
|
}
|
|
|
|
return retList, nil
|
|
}
|
|
|
|
func (c *PluginCatalog) ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
|
return c.listInternal(ctx, pluginType, true)
|
|
}
|
|
|
|
func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.PluginType, includeVersioned bool) ([]pluginutil.VersionedPlugin, error) {
|
|
c.lock.RLock()
|
|
defer c.lock.RUnlock()
|
|
|
|
var result []pluginutil.VersionedPlugin
|
|
|
|
// Collect keys for external plugins in the barrier.
|
|
keys, err := logical.CollectKeys(ctx, c.catalogView)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unversionedPlugins := make(map[string]struct{})
|
|
for _, key := range keys {
|
|
var semanticVersion *semver.Version
|
|
|
|
entry, err := c.catalogView.Get(ctx, key)
|
|
if err != nil || entry == nil {
|
|
continue
|
|
}
|
|
|
|
plugin := new(pluginutil.PluginRunner)
|
|
if err := jsonutil.DecodeJSON(entry.Value, plugin); err != nil {
|
|
return nil, fmt.Errorf("failed to decode plugin entry: %w", err)
|
|
}
|
|
|
|
if plugin.Version == "" {
|
|
semanticVersion, err = semver.NewVersion("0.0.0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if !includeVersioned {
|
|
continue
|
|
}
|
|
|
|
semanticVersion, err = semver.NewVersion(plugin.Version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unexpected error parsing version from plugin catalog entry %q: %w", key, err)
|
|
}
|
|
}
|
|
|
|
// Only list user-added plugins if they're of the given type.
|
|
if plugin.Type != consts.PluginTypeUnknown && plugin.Type != pluginType {
|
|
continue
|
|
}
|
|
|
|
result = append(result, pluginutil.VersionedPlugin{
|
|
Name: plugin.Name,
|
|
Type: plugin.Type.String(),
|
|
Version: plugin.Version,
|
|
SHA256: hex.EncodeToString(plugin.Sha256),
|
|
SemanticVersion: semanticVersion,
|
|
})
|
|
|
|
if plugin.Version == "" {
|
|
unversionedPlugins[plugin.Name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Get the builtin plugins.
|
|
builtinPlugins := c.builtinRegistry.Keys(pluginType)
|
|
for _, plugin := range builtinPlugins {
|
|
// Unversioned plugins fully replace builtins of the same name.
|
|
if _, ok := unversionedPlugins[plugin]; ok {
|
|
continue
|
|
}
|
|
|
|
version := versions.GetBuiltinVersion(pluginType, plugin)
|
|
semanticVersion, err := semver.NewVersion(version)
|
|
deprecationStatus, _ := c.builtinRegistry.DeprecationStatus(plugin, pluginType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, pluginutil.VersionedPlugin{
|
|
Name: plugin,
|
|
Type: pluginType.String(),
|
|
Version: version,
|
|
Builtin: true,
|
|
SemanticVersion: semanticVersion,
|
|
DeprecationStatus: deprecationStatus.String(),
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|