1115 lines
35 KiB
Go
1115 lines
35 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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
|
|
}
|