open-vault/vault/plugin_catalog.go

406 lines
12 KiB
Go
Raw Normal View History

2017-04-04 00:52:29 +00:00
package vault
import (
"context"
2017-04-04 00:52:29 +00:00
"encoding/json"
"errors"
"fmt"
"path/filepath"
"sort"
2017-04-04 00:52:29 +00:00
"strings"
"sync"
"github.com/hashicorp/errwrap"
2018-11-07 01:21:24 +00:00
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
2019-04-15 18:10:07 +00:00
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/newdbplugin"
"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"
2017-04-04 00:52:29 +00:00
)
var (
pluginCatalogPath = "core/plugin-catalog/"
ErrDirectoryNotConfigured = errors.New("could not set plugin, plugin directory is not configured")
Lazy-load plugin mounts (#3255) * Lazy load plugins to avoid setup-unwrap cycle * Remove commented blocks * Refactor NewTestCluster, use single core cluster on basic plugin tests * Set c.pluginDirectory in TestAddTestPlugin for setupPluginCatalog to work properly * Add special path to mock plugin * Move ensureCoresSealed to vault/testing.go * Use same method for EnsureCoresSealed and Cleanup * Bump ensureCoresSealed timeout to 60s * Correctly handle nil opts on NewTestCluster * Add metadata flag to APIClientMeta, use meta-enabled plugin when mounting to bootstrap * Check metadata flag directly on the plugin process * Plumb isMetadataMode down to PluginRunner * Add NOOP shims when running in metadata mode * Remove unused flag from the APIMetadata object * Remove setupSecretPlugins and setupCredentialPlugins functions * Move when we setup rollback manager to after the plugins are initialized * Fix tests * Fix merge issue * start rollback manager after the credential setup * Add guards against running certain client and server functions while in metadata mode * Call initialize once a plugin is loaded on the fly * Add more tests, update basic secret/auth plugin tests to trigger lazy loading * Skip mount if plugin removed from catalog * Fixup * Remove commented line on LookupPlugin * Fail on mount operation if plugin is re-added to catalog and mount is on existing path * Check type and special paths on startBackend * Fix merge conflicts * Refactor PluginRunner run methods to use runCommon, fix TestSystemBackend_Plugin_auth
2017-09-01 05:02:03 +00:00
ErrPluginNotFound = errors.New("plugin not found in the catalog")
ErrPluginBadType = errors.New("unable to determine plugin type")
2017-04-04 00:52:29 +00:00
)
// 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.
2017-04-04 00:52:29 +00:00
type PluginCatalog struct {
2018-11-07 01:21:24 +00:00
builtinRegistry BuiltinRegistry
catalogView *BarrierView
directory string
2017-04-04 00:52:29 +00:00
lock sync.RWMutex
2017-04-04 00:52:29 +00:00
}
2018-11-07 01:21:24 +00:00
func (c *Core) setupPluginCatalog(ctx context.Context) error {
2017-04-04 21:43:39 +00:00
c.pluginCatalog = &PluginCatalog{
2018-11-07 01:21:24 +00:00
builtinRegistry: c.builtinRegistry,
catalogView: NewBarrierView(c.barrier, pluginCatalogPath),
directory: c.pluginDirectory,
}
// 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)
2017-04-04 21:43:39 +00:00
}
2017-04-04 00:52:29 +00:00
Lazy-load plugin mounts (#3255) * Lazy load plugins to avoid setup-unwrap cycle * Remove commented blocks * Refactor NewTestCluster, use single core cluster on basic plugin tests * Set c.pluginDirectory in TestAddTestPlugin for setupPluginCatalog to work properly * Add special path to mock plugin * Move ensureCoresSealed to vault/testing.go * Use same method for EnsureCoresSealed and Cleanup * Bump ensureCoresSealed timeout to 60s * Correctly handle nil opts on NewTestCluster * Add metadata flag to APIClientMeta, use meta-enabled plugin when mounting to bootstrap * Check metadata flag directly on the plugin process * Plumb isMetadataMode down to PluginRunner * Add NOOP shims when running in metadata mode * Remove unused flag from the APIMetadata object * Remove setupSecretPlugins and setupCredentialPlugins functions * Move when we setup rollback manager to after the plugins are initialized * Fix tests * Fix merge issue * start rollback manager after the credential setup * Add guards against running certain client and server functions while in metadata mode * Call initialize once a plugin is loaded on the fly * Add more tests, update basic secret/auth plugin tests to trigger lazy loading * Skip mount if plugin removed from catalog * Fixup * Remove commented line on LookupPlugin * Fail on mount operation if plugin is re-added to catalog and mount is on existing path * Check type and special paths on startBackend * Fix merge conflicts * Refactor PluginRunner run methods to use runCommon, fix TestSystemBackend_Plugin_auth
2017-09-01 05:02:03 +00:00
if c.logger.IsInfo() {
c.logger.Info("successfully setup plugin catalog", "plugin-directory", c.pluginDirectory)
Lazy-load plugin mounts (#3255) * Lazy load plugins to avoid setup-unwrap cycle * Remove commented blocks * Refactor NewTestCluster, use single core cluster on basic plugin tests * Set c.pluginDirectory in TestAddTestPlugin for setupPluginCatalog to work properly * Add special path to mock plugin * Move ensureCoresSealed to vault/testing.go * Use same method for EnsureCoresSealed and Cleanup * Bump ensureCoresSealed timeout to 60s * Correctly handle nil opts on NewTestCluster * Add metadata flag to APIClientMeta, use meta-enabled plugin when mounting to bootstrap * Check metadata flag directly on the plugin process * Plumb isMetadataMode down to PluginRunner * Add NOOP shims when running in metadata mode * Remove unused flag from the APIMetadata object * Remove setupSecretPlugins and setupCredentialPlugins functions * Move when we setup rollback manager to after the plugins are initialized * Fix tests * Fix merge issue * start rollback manager after the credential setup * Add guards against running certain client and server functions while in metadata mode * Call initialize once a plugin is loaded on the fly * Add more tests, update basic secret/auth plugin tests to trigger lazy loading * Skip mount if plugin removed from catalog * Fixup * Remove commented line on LookupPlugin * Fail on mount operation if plugin is re-added to catalog and mount is on existing path * Check type and special paths on startBackend * Fix merge conflicts * Refactor PluginRunner run methods to use runCommon, fix TestSystemBackend_Plugin_auth
2017-09-01 05:02:03 +00:00
}
2017-04-04 00:52:29 +00:00
return nil
}
2018-11-07 01:21:24 +00:00
// 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. Both of these will be run in metadata mode.
func (c *PluginCatalog) getPluginTypeFromUnknown(ctx context.Context, logger log.Logger, plugin *pluginutil.PluginRunner) (consts.PluginType, error) {
merr := &multierror.Error{}
err := isDatabasePlugin(ctx, plugin)
if err == nil {
return consts.PluginTypeDatabase, nil
}
merr = multierror.Append(merr, err)
// Attempt to run as backend plugin
client, err := backendplugin.NewPluginClient(ctx, nil, plugin, log.NewNullLogger(), true)
if err == nil {
err := client.Setup(ctx, &logical.BackendConfig{})
if err != nil {
return consts.PluginTypeUnknown, err
}
backendType := client.Type()
client.Cleanup(ctx)
switch backendType {
case logical.TypeCredential:
return consts.PluginTypeCredential, nil
case logical.TypeLogical:
return consts.PluginTypeSecrets, nil
2018-11-07 01:21:24 +00:00
}
} else {
merr = multierror.Append(merr, err)
2018-11-07 01:21:24 +00:00
}
if client == nil || client.Type() == logical.TypeUnknown {
logger.Warn("unknown plugin type",
"plugin name", plugin.Name,
"error", merr.Error())
} else {
logger.Warn("unsupported plugin type",
"plugin name", plugin.Name,
"plugin type", client.Type().String(),
"error", merr.Error())
2018-11-07 01:21:24 +00:00
}
return consts.PluginTypeUnknown, nil
}
func isDatabasePlugin(ctx context.Context, plugin *pluginutil.PluginRunner) error {
merr := &multierror.Error{}
// Attempt to run as database V5 plugin
v5Client, err := newdbplugin.NewPluginClient(ctx, nil, plugin, log.NewNullLogger(), true)
if err == nil {
// Close the client and cleanup the plugin process
v5Client.Close()
return nil
}
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v5: %w", err))
v4Client, err := dbplugin.NewPluginClient(ctx, nil, plugin, log.NewNullLogger(), true)
if err == nil {
// Close the client and cleanup the plugin process
v4Client.Close()
return nil
}
merr = multierror.Append(merr, fmt.Errorf("failed to load plugin as database v4: %w", err))
return merr.ErrorOrNil()
}
2018-11-07 01:21:24 +00:00
// 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(errwrap.Wrapf("failed to load plugin entry: {{err}}", err))
continue
}
plugin := new(pluginutil.PluginRunner)
if err := jsonutil.DecodeJSON(pluginRaw.Value, plugin); err != nil {
retErr = multierror.Append(errwrap.Wrapf("failed to decode plugin entry: {{err}}", err))
continue
}
// prepend the plugin directory to the command
cmdOld := plugin.Command
plugin.Command = filepath.Join(c.directory, plugin.Command)
pluginType, err := c.getPluginTypeFromUnknown(ctx, logger, plugin)
2018-11-07 01:21:24 +00:00
if err != nil {
retErr = multierror.Append(retErr, fmt.Errorf("could not upgrade plugin %s: %s", pluginName, err))
continue
}
if pluginType == consts.PluginTypeUnknown {
retErr = multierror.Append(retErr, fmt.Errorf("could not upgrade plugin %s: plugin of unknown type", pluginName))
continue
}
// Upgrade the storage
err = c.setInternal(ctx, pluginName, pluginType, cmdOld, plugin.Args, plugin.Env, plugin.Sha256)
if err != nil {
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", pluginType.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.
2018-11-07 01:21:24 +00:00
func (c *PluginCatalog) Get(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) {
c.lock.RLock()
runner, err := c.get(ctx, name, pluginType)
c.lock.RUnlock()
return runner, err
}
func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) {
// If the directory isn't set only look for builtin plugins.
if c.directory != "" {
// Look for external plugins in the barrier
2018-11-07 01:21:24 +00:00
out, err := c.catalogView.Get(ctx, pluginType.String()+"/"+name)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("failed to retrieve plugin %q: {{err}}", name), err)
2017-04-04 21:43:39 +00:00
}
2018-11-07 01:21:24 +00:00
if out == nil {
// 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, errwrap.Wrapf(fmt.Sprintf("failed to retrieve plugin %q: {{err}}", name), err)
}
}
if out != nil {
entry := new(pluginutil.PluginRunner)
if err := jsonutil.DecodeJSON(out.Value, entry); err != nil {
return nil, errwrap.Wrapf("failed to decode plugin entry: {{err}}", err)
}
2018-11-07 01:21:24 +00:00
if entry.Type != pluginType && entry.Type != consts.PluginTypeUnknown {
return nil, nil
}
2017-04-04 21:43:39 +00:00
// prepend the plugin directory to the command
entry.Command = filepath.Join(c.directory, entry.Command)
return entry, nil
}
2017-04-04 00:52:29 +00:00
}
2017-04-04 21:43:39 +00:00
// Look for builtin plugins
2018-11-07 01:21:24 +00:00
if factory, ok := c.builtinRegistry.Get(name, pluginType); ok {
return &pluginutil.PluginRunner{
Name: name,
2018-11-07 01:21:24 +00:00
Type: pluginType,
Builtin: true,
BuiltinFactory: factory,
}, nil
2017-04-04 00:52:29 +00:00
}
2017-04-25 01:31:27 +00:00
return nil, nil
2017-04-04 00:52:29 +00:00
}
// 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.
2018-11-07 01:21:24 +00:00
func (c *PluginCatalog) Set(ctx context.Context, name string, pluginType consts.PluginType, 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()
2018-11-07 01:21:24 +00:00
return c.setInternal(ctx, name, pluginType, command, args, env, sha256)
}
func (c *PluginCatalog) setInternal(ctx context.Context, name string, pluginType consts.PluginType, command string, args []string, env []string, sha256 []byte) error {
2017-04-04 00:52:29 +00:00
// 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)
2017-04-04 00:52:29 +00:00
if err != nil {
return errwrap.Wrapf("error while validating the command path: {{err}}", err)
2017-04-04 00:52:29 +00:00
}
symAbs, err := filepath.Abs(filepath.Dir(sym))
if err != nil {
return errwrap.Wrapf("error while validating the command path: {{err}}", err)
2017-04-04 00:52:29 +00:00
}
if symAbs != c.directory {
2019-10-07 15:08:18 +00:00
return errors.New("cannot execute files outside of configured plugin directory")
2017-04-04 00:52:29 +00:00
}
2018-11-07 01:21:24 +00:00
// If the plugin type is unknown, we want to attempt to determine the type
if pluginType == consts.PluginTypeUnknown {
// entryTmp should only be used for the below type check, 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,
}
pluginType, err = c.getPluginTypeFromUnknown(ctx, log.Default(), entryTmp)
if err != nil {
return err
}
if pluginType == consts.PluginTypeUnknown {
return ErrPluginBadType
2018-11-07 01:21:24 +00:00
}
}
2017-04-04 00:52:29 +00:00
entry := &pluginutil.PluginRunner{
Name: name,
2018-11-07 01:21:24 +00:00
Type: pluginType,
Command: command,
Args: args,
Env: env,
2017-04-04 00:52:29 +00:00
Sha256: sha256,
2017-04-11 00:12:52 +00:00
Builtin: false,
2017-04-04 00:52:29 +00:00
}
buf, err := json.Marshal(entry)
if err != nil {
return errwrap.Wrapf("failed to encode plugin entry: {{err}}", err)
2017-04-04 00:52:29 +00:00
}
logicalEntry := logical.StorageEntry{
2018-11-07 01:21:24 +00:00
Key: pluginType.String() + "/" + name,
2017-04-04 00:52:29 +00:00
Value: buf,
}
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
return errwrap.Wrapf("failed to persist plugin entry: {{err}}", err)
2017-04-04 00:52:29 +00:00
}
return nil
}
// Delete is used to remove an external plugin from the catalog. Builtin plugins
// can not be deleted.
2018-11-07 01:21:24 +00:00
func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType) error {
c.lock.Lock()
defer c.lock.Unlock()
2018-11-07 01:21:24 +00:00
// Check the name under which the plugin exists, but if it's unfound, don't return any error.
pluginKey := pluginType.String() + "/" + name
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.
2018-11-07 01:21:24 +00:00
func (c *PluginCatalog) List(ctx context.Context, pluginType consts.PluginType) ([]string, error) {
c.lock.RLock()
defer c.lock.RUnlock()
// Collect keys for external plugins in the barrier.
keys, err := logical.CollectKeys(ctx, c.catalogView)
if err != nil {
return nil, err
}
2018-11-07 01:21:24 +00:00
// Get the builtin plugins.
builtinKeys := c.builtinRegistry.Keys(pluginType)
2018-11-07 01:21:24 +00:00
// Use a map to unique the two lists.
mapKeys := make(map[string]bool)
2018-11-07 01:21:24 +00:00
pluginTypePrefix := pluginType.String() + "/"
for _, plugin := range keys {
2018-11-07 01:21:24 +00:00
// Only list user-added plugins if they're of the given type.
if entry, err := c.get(ctx, plugin, pluginType); err == nil && entry != nil {
2018-11-07 01:21:24 +00:00
// Some keys will be prepended with the plugin type, but other ones won't.
// Users don't expect to see the plugin type, so we need to strip that here.
idx := strings.Index(plugin, pluginTypePrefix)
if idx == 0 {
plugin = plugin[len(pluginTypePrefix):]
}
mapKeys[plugin] = true
}
}
for _, plugin := range builtinKeys {
mapKeys[plugin] = true
}
retList := make([]string, len(mapKeys))
i := 0
for k := range mapKeys {
retList[i] = k
i++
}
2018-03-20 18:54:10 +00:00
// sort for consistent ordering of builtin plugins
sort.Strings(retList)
return retList, nil
}