1
0

Backend plugin system (#2874)

* Add backend plugin changes

* Fix totp backend plugin tests

* Fix logical/plugin InvalidateKey test

* Fix plugin catalog CRUD test, fix NoopBackend

* Clean up commented code block

* Fix system backend mount test

* Set plugin_name to omitempty, fix handleMountTable config parsing

* Clean up comments, keep shim connections alive until cleanup

* Include pluginClient, disallow LookupPlugin call from within a plugin

* Add wrapper around backendPluginClient for proper cleanup

* Add logger shim tests

* Add logger, storage, and system shim tests

* Use pointer receivers for system view shim

* Use plugin name if no path is provided on mount

* Enable plugins for auth backends

* Add backend type attribute, move builtin/plugin/package

* Fix merge conflict

* Fix missing plugin name in mount config

* Add integration tests on enabling auth backend plugins

* Remove dependency cycle on mock-plugin

* Add passthrough backend plugin, use logical.BackendType to determine lease generation

* Remove vault package dependency on passthrough package

* Add basic impl test for passthrough plugin

* Incorporate feedback; set b.backend after shims creation on backendPluginServer

* Fix totp plugin test

* Add plugin backends docs

* Fix tests

* Fix builtin/plugin tests

* Remove flatten from PluginRunner fields

* Move mock plugin to logical/plugin, remove totp and passthrough plugins

* Move pluginMap into newPluginClient

* Do not create storage RPC connection on HandleRequest and HandleExistenceCheck

* Change shim logger's Fatal to no-op

* Change BackendType to uint32, match UX backend types

* Change framework.Backend Setup signature

* Add Setup func to logical.Backend interface

* Move OptionallyEnableMlock call into plugin.Serve, update docs and comments

* Remove commented var in plugin package

* RegisterLicense on logical.Backend interface (#3017)

* Add RegisterLicense to logical.Backend interface

* Update RegisterLicense to use callback func on framework.Backend

* Refactor framework.Backend.RegisterLicense

* plugin: Prevent plugin.SystemViewClient.ResponseWrapData from getting JWTs

* plugin: Revert BackendType to remove TypePassthrough and related references

* Fix typo in plugin backends docs
This commit is contained in:
Calvin Leung Huang 2017-07-20 13:28:40 -04:00 committed by GitHub
parent 987616895d
commit 2b0f80b981
78 changed files with 2625 additions and 170 deletions

View File

@ -85,6 +85,7 @@ type EnableAuthOptions struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Local bool `json:"local" structs:"local"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type AuthMount struct {
@ -96,6 +97,7 @@ type AuthMount struct {
}
type AuthConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}

View File

@ -130,6 +130,7 @@ type MountConfigInput struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type MountOutput struct {
@ -141,7 +142,8 @@ type MountOutput struct {
}
type MountConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}

View File

@ -13,10 +13,13 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
if err != nil {
return nil, err
}
return b.Backend.Setup(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) (backend, error) {
func Backend(conf *logical.BackendConfig) (*backend, error) {
var b backend
b.MapAppId = &framework.PolicyMap{
PathMap: framework.PathMap{
@ -78,7 +81,7 @@ func Backend(conf *logical.BackendConfig) (backend, error) {
b.MapAppId.SaltFunc = b.Salt
b.MapUserId.SaltFunc = b.Salt
return b, nil
return &b, nil
}
type backend struct {

View File

@ -9,7 +9,7 @@ import (
)
func TestBackend_basic(t *testing.T) {
var b backend
var b *backend
var err error
var storage logical.Storage
factory := func(conf *logical.BackendConfig) (logical.Backend, error) {
@ -18,7 +18,10 @@ func TestBackend_basic(t *testing.T) {
t.Fatal(err)
}
storage = conf.StorageView
return b.Setup(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
logicaltest.Test(t, logicaltest.TestCase{
Factory: factory,

View File

@ -54,7 +54,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
if err != nil {
return nil, err
}
return b.Setup(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) (*backend, error) {

View File

@ -17,7 +17,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
if b == nil {
t.Fatalf("failed to create backend")
}
_, err = b.Backend.Setup(config)
err = b.Backend.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -17,7 +17,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
if err != nil {
return nil, err
}
return b.Setup(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
type backend struct {

View File

@ -29,7 +29,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -253,7 +253,7 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -307,7 +307,7 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -361,7 +361,7 @@ func TestBackend_TidyIdentities(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -386,7 +386,7 @@ func TestBackend_TidyRoleTags(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -411,7 +411,7 @@ func TestBackend_ConfigClient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -548,7 +548,7 @@ func TestBackend_pathConfigCertificate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -703,7 +703,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -784,7 +784,7 @@ func TestBackend_PathRoleTag(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -849,7 +849,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -997,7 +997,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing.
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -1177,7 +1177,7 @@ func TestBackend_pathStsConfig(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -1325,7 +1325,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -15,7 +15,7 @@ func TestBackend_pathConfigClient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -19,7 +19,7 @@ func TestBackend_pathRoleEc2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -146,7 +146,7 @@ func Test_enableIamIDResolution(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -221,7 +221,7 @@ func TestBackend_pathIam(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -385,7 +385,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -491,7 +491,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -617,7 +617,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -10,9 +10,8 @@ import (
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
_, err := b.Setup(conf)
if err != nil {
return b, err
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}

View File

@ -11,7 +11,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -13,7 +13,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -21,7 +21,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
t.Fatalf("failed to create backend")
}
_, err := b.Backend.Setup(config)
err := b.Backend.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -8,7 +8,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -7,7 +7,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -7,7 +7,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -9,7 +9,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -14,7 +14,7 @@ func TestBackend_PathListRoles(t *testing.T) {
config.StorageView = &logical.InmemStorage{}
b := Backend()
if _, err := b.Setup(config); err != nil {
if err := b.Setup(config); err != nil {
t.Fatal(err)
}

View File

@ -12,7 +12,11 @@ import (
// Factory creates a new backend
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
// Backend contains the base information for the backend's functionality

View File

@ -6,7 +6,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -16,7 +16,11 @@ import (
const databaseConfigPath = "database/config/"
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend(conf).Setup(conf)
b := Backend(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) *databaseBackend {

View File

@ -12,7 +12,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *framework.Backend {

View File

@ -12,7 +12,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -12,7 +12,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {

View File

@ -11,7 +11,11 @@ import (
// Factory creates a new backend implementing the logical.Backend interface
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
// Backend returns a new Backend framework struct

View File

@ -1870,7 +1870,7 @@ func TestBackend_PathFetchCertList(t *testing.T) {
config.StorageView = storage
b := Backend()
_, err := b.Setup(config)
err := b.Setup(config)
if err != nil {
t.Fatal(err)
}
@ -1997,7 +1997,7 @@ func TestBackend_SignVerbatim(t *testing.T) {
config.StorageView = storage
b := Backend()
_, err := b.Setup(config)
err := b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -13,7 +13,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
var err error
b := Backend()
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -13,7 +13,11 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend(conf).Setup(conf)
b := Backend(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) *backend {

View File

@ -13,7 +13,11 @@ import (
// Factory creates and configures the backend
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
// Creates a new backend with all the paths and secrets belonging to it

View File

@ -13,7 +13,7 @@ func TestBackend_config_lease_RU(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if _, err = b.Setup(config); err != nil {
if err = b.Setup(config); err != nil {
t.Fatal(err)
}

View File

@ -21,7 +21,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
if err != nil {
return nil, err
}
return b.Setup(conf)
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) (*backend, error) {

View File

@ -106,7 +106,7 @@ func TestBackend_allowed_users(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -17,7 +17,7 @@ func TestSSH_ConfigCAStorageUpgrade(t *testing.T) {
t.Fatal(err)
}
_, err = b.Setup(config)
err = b.Setup(config)
if err != nil {
t.Fatal(err)
}

View File

@ -10,10 +10,14 @@ import (
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend(conf).Setup(conf)
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) *backend {
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),

View File

@ -10,12 +10,10 @@ import (
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend(conf)
be, err := b.Backend.Setup(conf)
if err != nil {
if err := b.Setup(conf); err != nil {
return nil, err
}
return be, nil
return b, nil
}
func Backend(conf *logical.BackendConfig) *backend {

View File

@ -31,7 +31,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
if b == nil {
t.Fatalf("failed to create backend")
}
_, err := b.Backend.Setup(config)
err := b.Backend.Setup(config)
if err != nil {
t.Fatal(err)
}

39
builtin/plugin/backend.go Normal file
View File

@ -0,0 +1,39 @@
package plugin
import (
"fmt"
"github.com/hashicorp/vault/logical"
bplugin "github.com/hashicorp/vault/logical/plugin"
)
// Factory returns a configured plugin logical.Backend.
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
_, ok := conf.Config["plugin_name"]
if !ok {
return nil, fmt.Errorf("plugin_name not provided")
}
b, err := Backend(conf)
if err != nil {
return nil, err
}
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
// Backend returns an instance of the backend, either as a plugin if external
// or as a concrete implementation if builtin, casted as logical.Backend.
func Backend(conf *logical.BackendConfig) (logical.Backend, error) {
name := conf.Config["plugin_name"]
sys := conf.System
b, err := bplugin.NewBackend(name, sys)
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -0,0 +1,101 @@
package plugin
import (
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/plugin"
"github.com/hashicorp/vault/logical/plugin/mock"
"github.com/hashicorp/vault/vault"
)
func TestBackend(t *testing.T) {
config, cleanup := testConfig(t)
defer cleanup()
_, err := Backend(config)
if err != nil {
t.Fatal(err)
}
}
func TestBackend_Factory(t *testing.T) {
config, cleanup := testConfig(t)
defer cleanup()
_, err := Factory(config)
if err != nil {
t.Fatal(err)
}
}
func TestBackend_PluginMain(t *testing.T) {
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
return
}
content := []byte(vault.TestClusterCACert)
tmpfile, err := ioutil.TempFile("", "test-cacert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
args := []string{"--ca-cert=" + tmpfile.Name()}
apiClientMeta := &pluginutil.APIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(args)
tlsConfig := apiClientMeta.GetTLSConfig()
tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig)
err = plugin.Serve(&plugin.ServeOpts{
BackendFactoryFunc: mock.Factory,
TLSProviderFunc: tlsProviderFunc,
})
if err != nil {
t.Fatal(err)
}
}
func testConfig(t *testing.T) (*logical.BackendConfig, func()) {
coreConfig := &vault.CoreConfig{}
cluster := vault.NewTestCluster(t, coreConfig, true)
cluster.StartListeners()
cores := cluster.Cores
cores[0].Handler.Handle("/", http.Handler(cores[0].Core))
cores[1].Handler.Handle("/", http.Handler(cores[1].Core))
cores[2].Handler.Handle("/", http.Handler(cores[2].Core))
core := cores[0]
sys := vault.TestDynamicSystemView(core.Core)
config := &logical.BackendConfig{
Logger: nil,
System: sys,
Config: map[string]string{
"plugin_name": "mock-plugin",
},
}
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain")
return config, func() {
cluster.CloseListeners()
}
}

View File

@ -31,6 +31,7 @@ import (
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/totp"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/builtin/plugin"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/command"
@ -79,6 +80,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
"ldap": credLdap.Factory,
"okta": credOkta.Factory,
"radius": credRadius.Factory,
"plugin": plugin.Factory,
},
LogicalBackends: map[string]logical.Factory{
"aws": aws.Factory,
@ -94,6 +96,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
"rabbitmq": rabbitmq.Factory,
"database": database.Factory,
"totp": totp.Factory,
"plugin": plugin.Factory,
},
ShutdownCh: command.MakeShutdownCh(),
SighupCh: command.MakeSighupCh(),

View File

@ -14,11 +14,12 @@ type AuthEnableCommand struct {
}
func (c *AuthEnableCommand) Run(args []string) int {
var description, path string
var description, path, pluginName string
var local bool
flags := c.Meta.FlagSet("auth-enable", meta.FlagSetDefault)
flags.StringVar(&description, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.StringVar(&pluginName, "plugin-name", "", "")
flags.BoolVar(&local, "local", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
@ -36,8 +37,13 @@ func (c *AuthEnableCommand) Run(args []string) int {
authType := args[0]
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
if path == "" {
path = authType
if authType == "plugin" {
path = pluginName
} else {
path = authType
}
}
client, err := c.Client()
@ -50,6 +56,7 @@ func (c *AuthEnableCommand) Run(args []string) int {
if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{
Type: authType,
Description: description,
PluginName: pluginName,
Local: local,
}); err != nil {
c.Ui.Error(fmt.Sprintf(
@ -89,6 +96,9 @@ Auth Enable Options:
to the type of the mount. This will make the auth
provider available at "/auth/<path>"
-plugin-name Name of the auth plugin to use based from the name
in the plugin catalog.
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.

View File

@ -14,13 +14,14 @@ type MountCommand struct {
}
func (c *MountCommand) Run(args []string) int {
var description, path, defaultLeaseTTL, maxLeaseTTL string
var description, path, defaultLeaseTTL, maxLeaseTTL, pluginName string
var local, forceNoCache bool
flags := c.Meta.FlagSet("mount", meta.FlagSetDefault)
flags.StringVar(&description, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "")
flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "")
flags.StringVar(&pluginName, "plugin-name", "", "")
flags.BoolVar(&forceNoCache, "force-no-cache", false, "")
flags.BoolVar(&local, "local", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
@ -39,8 +40,13 @@ func (c *MountCommand) Run(args []string) int {
mountType := args[0]
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
if path == "" {
path = mountType
if mountType == "plugin" {
path = pluginName
} else {
path = mountType
}
}
client, err := c.Client()
@ -57,6 +63,7 @@ func (c *MountCommand) Run(args []string) int {
DefaultLeaseTTL: defaultLeaseTTL,
MaxLeaseTTL: maxLeaseTTL,
ForceNoCache: forceNoCache,
PluginName: pluginName,
},
Local: local,
}
@ -67,9 +74,14 @@ func (c *MountCommand) Run(args []string) int {
return 2
}
mountPart := fmt.Sprintf("'%s'", mountType)
if mountType == "plugin" {
mountPart = fmt.Sprintf("plugin '%s'", pluginName)
}
c.Ui.Output(fmt.Sprintf(
"Successfully mounted '%s' at '%s'!",
mountType, path))
"Successfully mounted %s at '%s'!",
mountPart, path))
return 0
}
@ -112,10 +124,12 @@ Mount Options:
not affect caching of the underlying encrypted
data storage.
-plugin-name Name of the plugin to mount based from the name
in the plugin catalog.
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.
`
return strings.TrimSpace(helpText)
}

View File

@ -42,9 +42,13 @@ func (c *MountsCommand) Run(args []string) int {
}
sort.Strings(paths)
columns := []string{"Path | Type | Accessor | Default TTL | Max TTL | Force No Cache | Replication Behavior | Description"}
columns := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Force No Cache | Replication Behavior | Description"}
for _, path := range paths {
mount := mounts[path]
pluginName := "n/a"
if mount.Config.PluginName != "" {
pluginName = mount.Config.PluginName
}
defTTL := "system"
switch {
case mount.Type == "system":
@ -68,7 +72,7 @@ func (c *MountsCommand) Run(args []string) int {
replicatedBehavior = "local"
}
columns = append(columns, fmt.Sprintf(
"%s | %s | %s | %s | %s | %v | %s | %s", path, mount.Type, mount.Accessor, defTTL, maxTTL,
"%s | %s | %s | %s | %s | %s | %v | %s | %s", path, mount.Type, mount.Accessor, pluginName, defTTL, maxTTL,
mount.Config.ForceNoCache, replicatedBehavior, mount.Description))
}

View File

@ -199,8 +199,8 @@ func TestRekey_init_pgp(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32,
},
}
sysBE := vault.NewSystemBackend(core)
sysBackend, err := sysBE.Backend.Setup(bc)
sysBackend := vault.NewSystemBackend(core)
err := sysBackend.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}

View File

@ -9,9 +9,11 @@ import (
"github.com/hashicorp/vault/plugins/database/postgresql"
)
// BuiltinFactory is the func signature that should be returned by
// the plugin's New() func.
type BuiltinFactory func() (interface{}, error)
var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{
var plugins = map[string]BuiltinFactory{
// These four plugins all use the same mysql implementation but with
// different username settings passed by the constructor.
"mysql-database-plugin": mysql.New(mysql.MetadataLen, mysql.UsernameLen),
@ -26,11 +28,14 @@ var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{
"hana-database-plugin": hana.New,
}
// Get returns the BuiltinFactory func for a particular backend plugin
// from the plugins map.
func Get(name string) (BuiltinFactory, bool) {
f, ok := plugins[name]
return f, ok
}
// Keys returns the list of plugin names that are considered builtin plugins.
func Keys() []string {
keys := make([]string, len(plugins))

View File

@ -35,12 +35,12 @@ type LookRunnerUtil interface {
// PluginRunner defines the metadata needed to run a plugin securely with
// go-plugin.
type PluginRunner struct {
Name string `json:"name"`
Command string `json:"command"`
Args []string `json:"args"`
Sha256 []byte `json:"sha256"`
Builtin bool `json:"builtin"`
BuiltinFactory func() (interface{}, error) `json:"-"`
Name string `json:"name" structs:"name"`
Command string `json:"command" structs:"command"`
Args []string `json:"args" structs:"args"`
Sha256 []byte `json:"sha256" structs:"sha256"`
Builtin bool `json:"builtin" structs:"builtin"`
BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"`
}
// Run takes a wrapper instance, and the go-plugin paramaters and executes a

View File

@ -126,7 +126,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er
// Parse the JWT and retrieve the vault address
wt, err := jws.ParseJWT([]byte(unwrapToken))
if err != nil {
return nil, errors.New(fmt.Sprintf("error decoding token: %s", err))
return nil, fmt.Errorf("error decoding token: %s", err)
}
if wt == nil {
return nil, errors.New("nil decoded token")
@ -146,7 +146,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er
// Sanity check the value
if _, err := url.Parse(vaultAddr); err != nil {
return nil, errors.New(fmt.Sprintf("error parsing the vault address: %s", err))
return nil, fmt.Errorf("error parsing the vault address: %s", err)
}
// Unwrap the token
@ -165,7 +165,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er
return nil, errwrap.Wrapf("error during token unwrap request: {{err}}", err)
}
if secret == nil {
return nil, errors.New("error during token unwrap request secret is nil")
return nil, errors.New("error during token unwrap request: secret is nil")
}
// Retrieve and parse the server's certificate

View File

@ -82,6 +82,12 @@ type Backend struct {
// See the built-in AuthRenew helpers in lease.go for common callbacks.
AuthRenew OperationFunc
// LicenseRegistration is called to register the license for a backend.
LicenseRegistration LicenseRegistrationFunc
// Type is the logical.BackendType for the backend implementation
BackendType logical.BackendType
logger log.Logger
system logical.SystemView
once sync.Once
@ -107,6 +113,10 @@ type InitializeFunc func() error
// InvalidateFunc is the callback for backend key invalidation.
type InvalidateFunc func(string)
// LicenseRegistrationFunc is the callback for backend license registration.
type LicenseRegistrationFunc func(interface{}) error
// HandleExistenceCheck is the logical.Backend implementation.
func (b *Backend) HandleExistenceCheck(req *logical.Request) (checkFound bool, exists bool, err error) {
b.once.Do(b.init)
@ -154,7 +164,7 @@ func (b *Backend) HandleExistenceCheck(req *logical.Request) (checkFound bool, e
return
}
// logical.Backend impl.
// HandleRequest is the logical.Backend implementation.
func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error) {
b.once.Do(b.init)
@ -221,18 +231,11 @@ func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error)
return callback(req, &fd)
}
// logical.Backend impl.
// SpecialPaths is the logical.Backend implementation.
func (b *Backend) SpecialPaths() *logical.Paths {
return b.PathsSpecial
}
// Setup is used to initialize the backend with the initial backend configuration
func (b *Backend) Setup(config *logical.BackendConfig) (logical.Backend, error) {
b.logger = config.Logger
b.system = config.System
return b, nil
}
// Cleanup is used to release resources and prepare to stop the backend
func (b *Backend) Cleanup() {
if b.Clean != nil {
@ -240,6 +243,7 @@ func (b *Backend) Cleanup() {
}
}
// Initialize calls the backend's Init func if set.
func (b *Backend) Initialize() error {
if b.Init != nil {
return b.Init()
@ -255,6 +259,13 @@ func (b *Backend) InvalidateKey(key string) {
}
}
// Setup is used to initialize the backend with the initial backend configuration
func (b *Backend) Setup(config *logical.BackendConfig) error {
b.logger = config.Logger
b.system = config.System
return nil
}
// Logger can be used to get the logger. If no logger has been set,
// the logs will be discarded.
func (b *Backend) Logger() log.Logger {
@ -265,11 +276,25 @@ func (b *Backend) Logger() log.Logger {
return logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelOff)
}
// System returns the backend's system view.
func (b *Backend) System() logical.SystemView {
return b.system
}
// This method takes in the TTL and MaxTTL values provided by the user,
// Type returns the backend type
func (b *Backend) Type() logical.BackendType {
return b.BackendType
}
// RegisterLicense performs backend license registration.
func (b *Backend) RegisterLicense(license interface{}) error {
if b.LicenseRegistration == nil {
return nil
}
return b.LicenseRegistration(license)
}
// SanitizeTTLStr takes in the TTL and MaxTTL values provided by the user,
// compares those with the SystemView values. If they are empty a value of 0 is
// set, which will cause initial secret or LeaseExtend operations to use the
// mount/system defaults. If they are set, their boundaries are validated.
@ -297,7 +322,8 @@ func (b *Backend) SanitizeTTLStr(ttlStr, maxTTLStr string) (ttl, maxTTL time.Dur
return
}
// Caps the boundaries of ttl and max_ttl values to the backend mount's max_ttl value.
// SanitizeTTL caps the boundaries of ttl and max_ttl values to the
// backend mount's max_ttl value.
func (b *Backend) SanitizeTTL(ttl, maxTTL time.Duration) (time.Duration, time.Duration, error) {
sysMaxTTL := b.System().MaxLeaseTTL()
if ttl > sysMaxTTL {
@ -575,6 +601,7 @@ func (s *FieldSchema) DefaultOrZero() interface{} {
return s.Type.Zero()
}
// Zero returns the correct zero-value for a specific FieldType
func (t FieldType) Zero() interface{} {
switch t {
case TypeString:

View File

@ -2,6 +2,29 @@ package logical
import log "github.com/mgutz/logxi/v1"
// BackendType is the type of backend that is being implemented
type BackendType uint32
// The these are the types of backends that can be derived from
// logical.Backend
const (
TypeUnknown BackendType = 0 // This is also the zero-value for BackendType
TypeLogical BackendType = 1
TypeCredential BackendType = 2
)
// Stringer implementation
func (b BackendType) String() string {
switch b {
case TypeLogical:
return "secret"
case TypeCredential:
return "auth"
}
return "unknown"
}
// Backend interface must be implemented to be "mountable" at
// a given path. Requests flow through a router which has various mount
// points that flow to a logical backend. The logic of each backend is flexible,
@ -27,6 +50,11 @@ type Backend interface {
// information, such as globally configured default and max lease TTLs.
System() SystemView
// Logger provides an interface to access the underlying logger. This
// is useful when a struct embeds a Backend-implemented struct that
// contains a private instance of logger.
Logger() log.Logger
// HandleExistenceCheck is used to handle a request and generate a response
// indicating whether the given path exists or not; this is used to
// understand whether the request must have a Create or Update capability
@ -47,6 +75,16 @@ type Backend interface {
// to the backend. The backend can use this to clear any caches or reset
// internal state as needed.
InvalidateKey(key string)
// Setup is used to set up the backend based on the provided backend
// configuration.
Setup(*BackendConfig) error
// Type returns the BackendType for the particular backend
Type() BackendType
// RegisterLicense performs backend license registration
RegisterLicense(interface{}) error
}
// BackendConfig is provided to the factory to initialize the backend

23
logical/plugin/backend.go Normal file
View File

@ -0,0 +1,23 @@
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/logical"
)
// BackendPlugin is the plugin.Plugin implementation
type BackendPlugin struct {
Factory func(*logical.BackendConfig) (logical.Backend, error)
}
// Server gets called when on plugin.Serve()
func (b *BackendPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) {
return &backendPluginServer{factory: b.Factory, broker: broker}, nil
}
// Client gets called on plugin.NewClient()
func (b BackendPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &backendPluginClient{client: c, broker: broker}, nil
}

View File

@ -0,0 +1,228 @@
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/logical"
log "github.com/mgutz/logxi/v1"
)
// backendPluginClient implements logical.Backend and is the
// go-plugin client.
type backendPluginClient struct {
broker *plugin.MuxBroker
client *rpc.Client
pluginClient *plugin.Client
system logical.SystemView
logger log.Logger
}
// HandleRequestArgs is the args for HandleRequest method.
type HandleRequestArgs struct {
StorageID uint32
Request *logical.Request
}
// HandleRequestReply is the reply for HandleRequest method.
type HandleRequestReply struct {
Response *logical.Response
Error *plugin.BasicError
}
// SpecialPathsReply is the reply for SpecialPaths method.
type SpecialPathsReply struct {
Paths *logical.Paths
}
// SystemReply is the reply for System method.
type SystemReply struct {
SystemView logical.SystemView
Error *plugin.BasicError
}
// HandleExistenceCheckArgs is the args for HandleExistenceCheck method.
type HandleExistenceCheckArgs struct {
StorageID uint32
Request *logical.Request
}
// HandleExistenceCheckReply is the reply for HandleExistenceCheck method.
type HandleExistenceCheckReply struct {
CheckFound bool
Exists bool
Error *plugin.BasicError
}
// SetupArgs is the args for Setup method.
type SetupArgs struct {
StorageID uint32
LoggerID uint32
SysViewID uint32
Config map[string]string
}
// SetupReply is the reply for Setup method.
type SetupReply struct {
Error *plugin.BasicError
}
// TypeReply is the reply for the Type method.
type TypeReply struct {
Type logical.BackendType
}
// RegisterLicenseArgs is the args for the RegisterLicense method.
type RegisterLicenseArgs struct {
License interface{}
}
// RegisterLicenseReply is the reply for the RegisterLicense method.
type RegisterLicenseReply struct {
Error *plugin.BasicError
}
func (b *backendPluginClient) HandleRequest(req *logical.Request) (*logical.Response, error) {
args := &HandleRequestArgs{
Request: req,
}
var reply HandleRequestReply
err := b.client.Call("Plugin.HandleRequest", args, &reply)
if err != nil {
return nil, err
}
if reply.Error != nil {
if reply.Error.Error() == logical.ErrUnsupportedOperation.Error() {
return nil, logical.ErrUnsupportedOperation
}
return nil, reply.Error
}
return reply.Response, nil
}
func (b *backendPluginClient) SpecialPaths() *logical.Paths {
var reply SpecialPathsReply
err := b.client.Call("Plugin.SpecialPaths", new(interface{}), &reply)
if err != nil {
return nil
}
return reply.Paths
}
// System returns vault's system view. The backend client stores the view during
// Setup, so there is no need to shim the system just to get it back.
func (b *backendPluginClient) System() logical.SystemView {
return b.system
}
// Logger returns vault's logger. The backend client stores the logger during
// Setup, so there is no need to shim the logger just to get it back.
func (b *backendPluginClient) Logger() log.Logger {
return b.logger
}
func (b *backendPluginClient) HandleExistenceCheck(req *logical.Request) (bool, bool, error) {
args := &HandleExistenceCheckArgs{
Request: req,
}
var reply HandleExistenceCheckReply
err := b.client.Call("Plugin.HandleExistenceCheck", args, &reply)
if err != nil {
return false, false, err
}
if reply.Error != nil {
// THINKING: Should be be a switch on all error types?
if reply.Error.Error() == logical.ErrUnsupportedPath.Error() {
return false, false, logical.ErrUnsupportedPath
}
return false, false, reply.Error
}
return reply.CheckFound, reply.Exists, nil
}
func (b *backendPluginClient) Cleanup() {
b.client.Call("Plugin.Cleanup", new(interface{}), &struct{}{})
}
func (b *backendPluginClient) Initialize() error {
err := b.client.Call("Plugin.Initialize", new(interface{}), &struct{}{})
return err
}
func (b *backendPluginClient) InvalidateKey(key string) {
b.client.Call("Plugin.InvalidateKey", key, &struct{}{})
}
func (b *backendPluginClient) Setup(config *logical.BackendConfig) error {
// Shim logical.Storage
storageID := b.broker.NextId()
go b.broker.AcceptAndServe(storageID, &StorageServer{
impl: config.StorageView,
})
// Shim log.Logger
loggerID := b.broker.NextId()
go b.broker.AcceptAndServe(loggerID, &LoggerServer{
logger: config.Logger,
})
// Shim logical.SystemView
sysViewID := b.broker.NextId()
go b.broker.AcceptAndServe(sysViewID, &SystemViewServer{
impl: config.System,
})
args := &SetupArgs{
StorageID: storageID,
LoggerID: loggerID,
SysViewID: sysViewID,
Config: config.Config,
}
var reply SetupReply
err := b.client.Call("Plugin.Setup", args, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
// Set system and logger for getter methods
b.system = config.System
b.logger = config.Logger
return nil
}
func (b *backendPluginClient) Type() logical.BackendType {
var reply TypeReply
err := b.client.Call("Plugin.Type", new(interface{}), &reply)
if err != nil {
return logical.TypeUnknown
}
return logical.BackendType(reply.Type)
}
func (b *backendPluginClient) RegisterLicense(license interface{}) error {
var reply RegisterLicenseReply
args := RegisterLicenseArgs{
License: license,
}
err := b.client.Call("Plugin.RegisterLicense", args, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
return nil
}

View File

@ -0,0 +1,156 @@
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/logical"
)
// backendPluginServer is the RPC server that backendPluginClient talks to,
// it methods conforming to requirements by net/rpc
type backendPluginServer struct {
broker *plugin.MuxBroker
backend logical.Backend
factory func(*logical.BackendConfig) (logical.Backend, error)
loggerClient *rpc.Client
sysViewClient *rpc.Client
storageClient *rpc.Client
}
func (b *backendPluginServer) HandleRequest(args *HandleRequestArgs, reply *HandleRequestReply) error {
storage := &StorageClient{client: b.storageClient}
args.Request.Storage = storage
resp, err := b.backend.HandleRequest(args.Request)
*reply = HandleRequestReply{
Response: resp,
Error: plugin.NewBasicError(err),
}
return nil
}
func (b *backendPluginServer) SpecialPaths(_ interface{}, reply *SpecialPathsReply) error {
*reply = SpecialPathsReply{
Paths: b.backend.SpecialPaths(),
}
return nil
}
func (b *backendPluginServer) HandleExistenceCheck(args *HandleExistenceCheckArgs, reply *HandleExistenceCheckReply) error {
storage := &StorageClient{client: b.storageClient}
args.Request.Storage = storage
checkFound, exists, err := b.backend.HandleExistenceCheck(args.Request)
*reply = HandleExistenceCheckReply{
CheckFound: checkFound,
Exists: exists,
Error: plugin.NewBasicError(err),
}
return nil
}
func (b *backendPluginServer) Cleanup(_ interface{}, _ *struct{}) error {
b.backend.Cleanup()
// Close rpc clients
b.loggerClient.Close()
b.sysViewClient.Close()
b.storageClient.Close()
return nil
}
func (b *backendPluginServer) Initialize(_ interface{}, _ *struct{}) error {
err := b.backend.Initialize()
return err
}
func (b *backendPluginServer) InvalidateKey(args string, _ *struct{}) error {
b.backend.InvalidateKey(args)
return nil
}
// Setup dials into the plugin's broker to get a shimmed storage, logger, and
// system view of the backend. This method also instantiates the underlying
// backend through its factory func for the server side of the plugin.
func (b *backendPluginServer) Setup(args *SetupArgs, reply *SetupReply) error {
// Dial for storage
storageConn, err := b.broker.Dial(args.StorageID)
if err != nil {
*reply = SetupReply{
Error: plugin.NewBasicError(err),
}
return nil
}
rawStorageClient := rpc.NewClient(storageConn)
b.storageClient = rawStorageClient
storage := &StorageClient{client: rawStorageClient}
// Dial for logger
loggerConn, err := b.broker.Dial(args.LoggerID)
if err != nil {
*reply = SetupReply{
Error: plugin.NewBasicError(err),
}
return nil
}
rawLoggerClient := rpc.NewClient(loggerConn)
b.loggerClient = rawLoggerClient
logger := &LoggerClient{client: rawLoggerClient}
// Dial for sys view
sysViewConn, err := b.broker.Dial(args.SysViewID)
if err != nil {
*reply = SetupReply{
Error: plugin.NewBasicError(err),
}
return nil
}
rawSysViewClient := rpc.NewClient(sysViewConn)
b.sysViewClient = rawSysViewClient
sysView := &SystemViewClient{client: rawSysViewClient}
config := &logical.BackendConfig{
StorageView: storage,
Logger: logger,
System: sysView,
Config: args.Config,
}
// Call the underlying backend factory after shims have been created
// to set b.backend
backend, err := b.factory(config)
if err != nil {
*reply = SetupReply{
Error: plugin.NewBasicError(err),
}
}
b.backend = backend
return nil
}
func (b *backendPluginServer) Type(_ interface{}, reply *TypeReply) error {
*reply = TypeReply{
Type: b.backend.Type(),
}
return nil
}
func (b *backendPluginServer) RegisterLicense(args *RegisterLicenseArgs, reply *RegisterLicenseReply) error {
err := b.backend.RegisterLicense(args.License)
if err != nil {
*reply = RegisterLicenseReply{
Error: plugin.NewBasicError(err),
}
}
return nil
}

View File

@ -0,0 +1,176 @@
package plugin
import (
"testing"
"time"
gplugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/logformat"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/plugin/mock"
log "github.com/mgutz/logxi/v1"
)
func TestBackendPlugin_impl(t *testing.T) {
var _ gplugin.Plugin = new(BackendPlugin)
var _ logical.Backend = new(backendPluginClient)
}
func TestBackendPlugin_HandleRequest(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
resp, err := b.HandleRequest(&logical.Request{
Operation: logical.ReadOperation,
Path: "test/ing",
Data: map[string]interface{}{"value": "foo"},
})
if err != nil {
t.Fatal(err)
}
if resp.Data["value"] != "foo" {
t.Fatalf("bad: %#v", resp)
}
}
func TestBackendPlugin_SpecialPaths(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
paths := b.SpecialPaths()
if paths == nil {
t.Fatal("SpecialPaths() returned nil")
}
}
func TestBackendPlugin_System(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
sys := b.System()
if sys == nil {
t.Fatal("System() returned nil")
}
actual := sys.DefaultLeaseTTL()
expected := 300 * time.Second
if actual != expected {
t.Fatalf("bad: %v, expected %v", actual, expected)
}
}
func TestBackendPlugin_Logger(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
logger := b.Logger()
if logger == nil {
t.Fatal("Logger() returned nil")
}
}
func TestBackendPlugin_HandleExistenceCheck(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{
Operation: logical.CreateOperation,
Path: "test/ing",
Data: map[string]interface{}{"value": "foo"},
})
if err != nil {
t.Fatal(err)
}
if !checkFound {
t.Fatal("existence check not found for path 'test/ing'")
}
if exists {
t.Fatal("existence check should have returned 'false' for 'testing/read'")
}
}
func TestBackendPlugin_Cleanup(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
b.Cleanup()
}
func TestBackendPlugin_Initialize(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
err := b.Initialize()
if err != nil {
t.Fatal(err)
}
}
func TestBackendPlugin_InvalidateKey(t *testing.T) {
b, cleanup := testBackend(t)
defer cleanup()
resp, err := b.HandleRequest(&logical.Request{
Operation: logical.ReadOperation,
Path: "internal",
})
if err != nil {
t.Fatal(err)
}
if resp.Data["value"] == "" {
t.Fatalf("bad: %#v, expected non-empty value", resp)
}
b.InvalidateKey("internal")
resp, err = b.HandleRequest(&logical.Request{
Operation: logical.ReadOperation,
Path: "internal",
})
if err != nil {
t.Fatal(err)
}
if resp.Data["value"] != "" {
t.Fatalf("bad: expected empty response data, got %#v", resp)
}
}
func TestBackendPlugin_Setup(t *testing.T) {
_, cleanup := testBackend(t)
defer cleanup()
}
func testBackend(t *testing.T) (logical.Backend, func()) {
// Create a mock provider
pluginMap := map[string]gplugin.Plugin{
"backend": &BackendPlugin{
Factory: mock.Factory,
},
}
client, _ := gplugin.TestPluginRPCConn(t, pluginMap)
cleanup := func() {
client.Close()
}
// Request the backend
raw, err := client.Dispense(BackendPluginName)
if err != nil {
t.Fatal(err)
}
b := raw.(logical.Backend)
err = b.Setup(&logical.BackendConfig{
Logger: logformat.NewVaultLogger(log.LevelTrace),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 300 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
StorageView: &logical.InmemStorage{},
})
if err != nil {
t.Fatal(err)
}
return b, cleanup
}

205
logical/plugin/logger.go Normal file
View File

@ -0,0 +1,205 @@
package plugin
import (
"net/rpc"
plugin "github.com/hashicorp/go-plugin"
log "github.com/mgutz/logxi/v1"
)
type LoggerClient struct {
client *rpc.Client
}
func (l *LoggerClient) Trace(msg string, args ...interface{}) {
cArgs := &LoggerArgs{
Msg: msg,
Args: args,
}
l.client.Call("Plugin.Trace", cArgs, &struct{}{})
}
func (l *LoggerClient) Debug(msg string, args ...interface{}) {
cArgs := &LoggerArgs{
Msg: msg,
Args: args,
}
l.client.Call("Plugin.Debug", cArgs, &struct{}{})
}
func (l *LoggerClient) Info(msg string, args ...interface{}) {
cArgs := &LoggerArgs{
Msg: msg,
Args: args,
}
l.client.Call("Plugin.Info", cArgs, &struct{}{})
}
func (l *LoggerClient) Warn(msg string, args ...interface{}) error {
var reply LoggerReply
cArgs := &LoggerArgs{
Msg: msg,
Args: args,
}
err := l.client.Call("Plugin.Warn", cArgs, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
return nil
}
func (l *LoggerClient) Error(msg string, args ...interface{}) error {
var reply LoggerReply
cArgs := &LoggerArgs{
Msg: msg,
Args: args,
}
err := l.client.Call("Plugin.Error", cArgs, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
return nil
}
func (l *LoggerClient) Fatal(msg string, args ...interface{}) {
// NOOP since it's not actually used within vault
return
}
func (l *LoggerClient) Log(level int, msg string, args []interface{}) {
cArgs := &LoggerArgs{
Level: level,
Msg: msg,
Args: args,
}
l.client.Call("Plugin.Log", cArgs, &struct{}{})
}
func (l *LoggerClient) SetLevel(level int) {
l.client.Call("Plugin.SetLevel", level, &struct{}{})
}
func (l *LoggerClient) IsTrace() bool {
var reply LoggerReply
l.client.Call("Plugin.IsTrace", new(interface{}), &reply)
return reply.IsTrue
}
func (l *LoggerClient) IsDebug() bool {
var reply LoggerReply
l.client.Call("Plugin.IsDebug", new(interface{}), &reply)
return reply.IsTrue
}
func (l *LoggerClient) IsInfo() bool {
var reply LoggerReply
l.client.Call("Plugin.IsInfo", new(interface{}), &reply)
return reply.IsTrue
}
func (l *LoggerClient) IsWarn() bool {
var reply LoggerReply
l.client.Call("Plugin.IsWarn", new(interface{}), &reply)
return reply.IsTrue
}
type LoggerServer struct {
logger log.Logger
}
func (l *LoggerServer) Trace(args *LoggerArgs, _ *struct{}) error {
l.logger.Trace(args.Msg, args.Args)
return nil
}
func (l *LoggerServer) Debug(args *LoggerArgs, _ *struct{}) error {
l.logger.Debug(args.Msg, args.Args)
return nil
}
func (l *LoggerServer) Info(args *LoggerArgs, _ *struct{}) error {
l.logger.Info(args.Msg, args.Args)
return nil
}
func (l *LoggerServer) Warn(args *LoggerArgs, reply *LoggerReply) error {
err := l.logger.Warn(args.Msg, args.Args)
if err != nil {
*reply = LoggerReply{
Error: plugin.NewBasicError(err),
}
return nil
}
return nil
}
func (l *LoggerServer) Error(args *LoggerArgs, reply *LoggerReply) error {
err := l.logger.Error(args.Msg, args.Args)
if err != nil {
*reply = LoggerReply{
Error: plugin.NewBasicError(err),
}
return nil
}
return nil
}
func (l *LoggerServer) Log(args *LoggerArgs, _ *struct{}) error {
l.logger.Log(args.Level, args.Msg, args.Args)
return nil
}
func (l *LoggerServer) SetLevel(args int, _ *struct{}) error {
l.logger.SetLevel(args)
return nil
}
func (l *LoggerServer) IsTrace(args interface{}, reply *LoggerReply) error {
result := l.logger.IsTrace()
*reply = LoggerReply{
IsTrue: result,
}
return nil
}
func (l *LoggerServer) IsDebug(args interface{}, reply *LoggerReply) error {
result := l.logger.IsDebug()
*reply = LoggerReply{
IsTrue: result,
}
return nil
}
func (l *LoggerServer) IsInfo(args interface{}, reply *LoggerReply) error {
result := l.logger.IsInfo()
*reply = LoggerReply{
IsTrue: result,
}
return nil
}
func (l *LoggerServer) IsWarn(args interface{}, reply *LoggerReply) error {
result := l.logger.IsWarn()
*reply = LoggerReply{
IsTrue: result,
}
return nil
}
type LoggerArgs struct {
Level int
Msg string
Args []interface{}
}
// LoggerReply contains the RPC reply. Not all fields may be used
// for a particular RPC call.
type LoggerReply struct {
IsTrue bool
Error *plugin.BasicError
}

View File

@ -0,0 +1,163 @@
package plugin
import (
"bufio"
"bytes"
"io/ioutil"
"strings"
"testing"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/logformat"
log "github.com/mgutz/logxi/v1"
)
func TestLogger_impl(t *testing.T) {
var _ log.Logger = new(LoggerClient)
}
func TestLogger_levels(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
l := logformat.NewVaultLoggerWithWriter(writer, log.LevelTrace)
server.RegisterName("Plugin", &LoggerServer{
logger: l,
})
expected := "foobar"
testLogger := &LoggerClient{client: client}
// Test trace
testLogger.Trace(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result := buf.String()
buf.Reset()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
// Test debug
testLogger.Debug(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result = buf.String()
buf.Reset()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
// Test debug
testLogger.Info(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result = buf.String()
buf.Reset()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
// Test warn
testLogger.Warn(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result = buf.String()
buf.Reset()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
// Test error
testLogger.Error(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result = buf.String()
buf.Reset()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
// Test fatal
testLogger.Fatal(expected)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result = buf.String()
buf.Reset()
if result != "" {
t.Fatalf("expected log Fatal() to be no-op, got %s", result)
}
}
func TestLogger_isLevels(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
l := logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelAll)
server.RegisterName("Plugin", &LoggerServer{
logger: l,
})
testLogger := &LoggerClient{client: client}
if !testLogger.IsDebug() || !testLogger.IsInfo() || !testLogger.IsTrace() || !testLogger.IsWarn() {
t.Fatal("expected logger to return true for all logger level checks")
}
}
func TestLogger_log(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
l := logformat.NewVaultLoggerWithWriter(writer, log.LevelTrace)
server.RegisterName("Plugin", &LoggerServer{
logger: l,
})
expected := "foobar"
testLogger := &LoggerClient{client: client}
// Test trace
testLogger.Log(log.LevelInfo, expected, nil)
if err := writer.Flush(); err != nil {
t.Fatal(err)
}
result := buf.String()
if !strings.Contains(result, expected) {
t.Fatalf("expected log to contain %s, got %s", expected, result)
}
}
func TestLogger_setLevel(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
l := log.NewLogger(ioutil.Discard, "test-logger")
server.RegisterName("Plugin", &LoggerServer{
logger: l,
})
testLogger := &LoggerClient{client: client}
testLogger.SetLevel(log.LevelWarn)
if !testLogger.IsWarn() {
t.Fatal("expected logger to support warn level")
}
}

View File

@ -0,0 +1,69 @@
package mock
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// New returns a new backend as an interface. This func
// is only necessary for builtin backend plugins.
func New() (interface{}, error) {
return Backend(), nil
}
// Factory returns a new backend as logical.Backend.
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
// FactoryType is a wrapper func that allows the Factory func to specify
// the backend type for the mock backend plugin instance.
func FactoryType(backendType logical.BackendType) func(*logical.BackendConfig) (logical.Backend, error) {
return func(conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
b.BackendType = backendType
if err := b.Setup(conf); err != nil {
return nil, err
}
return b, nil
}
}
// Backend returns a private embedded struct of framework.Backend.
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Help: "",
Paths: []*framework.Path{
pathTesting(&b),
pathInternal(&b),
},
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"special",
},
},
Secrets: []*framework.Secret{},
Invalidate: b.invalidate,
}
b.internal = "bar"
return &b
}
type backend struct {
*framework.Backend
// internal is used to test invalidate
internal string
}
func (b *backend) invalidate(key string) {
switch key {
case "internal":
b.internal = ""
}
}

View File

@ -0,0 +1,11 @@
package mock
import (
"testing"
"github.com/hashicorp/vault/logical"
)
func TestMockBackend_impl(t *testing.T) {
var _ logical.Backend = new(backend)
}

View File

@ -0,0 +1,28 @@
package main
import (
"log"
"os"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/logical/plugin"
"github.com/hashicorp/vault/logical/plugin/mock"
)
func main() {
apiClientMeta := &pluginutil.APIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args)
tlsConfig := apiClientMeta.GetTLSConfig()
tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig)
err := plugin.Serve(&plugin.ServeOpts{
BackendFactoryFunc: mock.Factory,
TLSProviderFunc: tlsProviderFunc,
})
if err != nil {
log.Println(err)
os.Exit(1)
}
}

View File

@ -0,0 +1,28 @@
package mock
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathInternal(b *backend) *framework.Path {
return &framework.Path{
Pattern: "internal",
Fields: map[string]*framework.FieldSchema{},
ExistenceCheck: b.pathTestingExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathTestingReadInternal,
},
}
}
func (b *backend) pathTestingReadInternal(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Return the secret
return &logical.Response{
Data: map[string]interface{}{
"value": b.internal,
},
}, nil
}

View File

@ -0,0 +1,56 @@
package mock
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathTesting(b *backend) *framework.Path {
return &framework.Path{
Pattern: "test/ing",
Fields: map[string]*framework.FieldSchema{
"value": &framework.FieldSchema{Type: framework.TypeString},
},
ExistenceCheck: b.pathTestingExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathTestingRead,
logical.CreateOperation: b.pathTestingCreate,
},
}
}
func (b *backend) pathTestingRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Return the secret
return &logical.Response{
Data: map[string]interface{}{
"value": data.Get("value"),
},
}, nil
}
func (b *backend) pathTestingCreate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
val := data.Get("value").(string)
entry := &logical.StorageEntry{
Key: "test/ing",
Value: []byte(val),
}
s := req.Storage
err := s.Put(entry)
if err != nil {
return nil, err
}
return &logical.Response{
Data: map[string]interface{}{
"value": data.Get("value"),
},
}, nil
}
func (b *backend) pathTestingExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
return false, nil
}

96
logical/plugin/plugin.go Normal file
View File

@ -0,0 +1,96 @@
package plugin
import (
"fmt"
"sync"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/logical"
)
// BackendPluginClient is a wrapper around backendPluginClient
// that also contains its plugin.Client instance. It's primarily
// used to cleanly kill the client on Cleanup()
type BackendPluginClient struct {
client *plugin.Client
sync.Mutex
*backendPluginClient
}
// Cleanup calls the RPC client's Cleanup() func and also calls
// the go-plugin's client Kill() func
func (b *BackendPluginClient) Cleanup() {
b.backendPluginClient.Cleanup()
b.client.Kill()
}
// NewBackend will return an instance of an RPC-based client implementation of the backend for
// external plugins, or a concrete implementation of the backend if it is a builtin backend.
// The backend is returned as a logical.Backend interface.
func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil) (logical.Backend, error) {
// Look for plugin in the plugin catalog
pluginRunner, err := sys.LookupPlugin(pluginName)
if err != nil {
return nil, err
}
var backend logical.Backend
if pluginRunner.Builtin {
// Plugin is builtin so we can retrieve an instance of the interface
// from the pluginRunner. Then cast it to logical.Backend.
backendRaw, err := pluginRunner.BuiltinFactory()
if err != nil {
return nil, fmt.Errorf("error getting plugin type: %s", err)
}
var ok bool
backend, ok = backendRaw.(logical.Backend)
if !ok {
return nil, fmt.Errorf("unsuported backend type: %s", pluginName)
}
} else {
// create a backendPluginClient instance
backend, err = newPluginClient(sys, pluginRunner)
if err != nil {
return nil, err
}
}
return backend, nil
}
func newPluginClient(sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner) (logical.Backend, error) {
// pluginMap is the map of plugins we can dispense.
pluginMap := map[string]plugin.Plugin{
"backend": &BackendPlugin{},
}
client, err := pluginRunner.Run(sys, pluginMap, handshakeConfig, []string{})
if err != nil {
return nil, err
}
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
// Request the plugin
raw, err := rpcClient.Dispense("backend")
if err != nil {
return nil, err
}
// We should have a logical backend type now. This feels like a normal interface
// implementation but is in fact over an RPC connection.
backendRPC := raw.(*backendPluginClient)
return &BackendPluginClient{
client: client,
backendPluginClient: backendRPC,
}, nil
}

54
logical/plugin/serve.go Normal file
View File

@ -0,0 +1,54 @@
package plugin
import (
"crypto/tls"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/logical"
)
// BackendPluginName is the name of the plugin that can be
// dispensed rom the plugin server.
const BackendPluginName = "backend"
type BackendFactoryFunc func(*logical.BackendConfig) (logical.Backend, error)
type TLSProdiverFunc func() (*tls.Config, error)
type ServeOpts struct {
BackendFactoryFunc BackendFactoryFunc
TLSProviderFunc TLSProdiverFunc
}
// Serve is used to serve a backend plugin
func Serve(opts *ServeOpts) error {
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"backend": &BackendPlugin{
Factory: opts.BackendFactoryFunc,
},
}
err := pluginutil.OptionallyEnableMlock()
if err != nil {
return err
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
TLSProvider: opts.TLSProviderFunc,
})
return nil
}
// handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "VAULT_BACKEND_PLUGIN",
MagicCookieValue: "6669da05-b1c8-4f49-97d9-c8e5bed98e20",
}

119
logical/plugin/storage.go Normal file
View File

@ -0,0 +1,119 @@
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/logical"
)
// StorageClient is an implementation of logical.Storage that communicates
// over RPC.
type StorageClient struct {
client *rpc.Client
}
func (s *StorageClient) List(prefix string) ([]string, error) {
var reply StorageListReply
err := s.client.Call("Plugin.List", prefix, &reply)
if err != nil {
return reply.Keys, err
}
if reply.Error != nil {
return reply.Keys, reply.Error
}
return reply.Keys, nil
}
func (s *StorageClient) Get(key string) (*logical.StorageEntry, error) {
var reply StorageGetReply
err := s.client.Call("Plugin.Get", key, &reply)
if err != nil {
return nil, err
}
if reply.Error != nil {
return nil, reply.Error
}
return reply.StorageEntry, nil
}
func (s *StorageClient) Put(entry *logical.StorageEntry) error {
var reply StoragePutReply
err := s.client.Call("Plugin.Put", entry, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
return nil
}
func (s *StorageClient) Delete(key string) error {
var reply StorageDeleteReply
err := s.client.Call("Plugin.Delete", key, &reply)
if err != nil {
return err
}
if reply.Error != nil {
return reply.Error
}
return nil
}
// StorageServer is a net/rpc compatible structure for serving
type StorageServer struct {
impl logical.Storage
}
func (s *StorageServer) List(prefix string, reply *StorageListReply) error {
keys, err := s.impl.List(prefix)
*reply = StorageListReply{
Keys: keys,
Error: plugin.NewBasicError(err),
}
return nil
}
func (s *StorageServer) Get(key string, reply *StorageGetReply) error {
storageEntry, err := s.impl.Get(key)
*reply = StorageGetReply{
StorageEntry: storageEntry,
Error: plugin.NewBasicError(err),
}
return nil
}
func (s *StorageServer) Put(entry *logical.StorageEntry, reply *StoragePutReply) error {
err := s.impl.Put(entry)
*reply = StoragePutReply{
Error: plugin.NewBasicError(err),
}
return nil
}
func (s *StorageServer) Delete(key string, reply *StorageDeleteReply) error {
err := s.impl.Delete(key)
*reply = StorageDeleteReply{
Error: plugin.NewBasicError(err),
}
return nil
}
type StorageListReply struct {
Keys []string
Error *plugin.BasicError
}
type StorageGetReply struct {
StorageEntry *logical.StorageEntry
Error *plugin.BasicError
}
type StoragePutReply struct {
Error *plugin.BasicError
}
type StorageDeleteReply struct {
Error *plugin.BasicError
}

View File

@ -0,0 +1,27 @@
package plugin
import (
"testing"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/logical"
)
func TestStorage_impl(t *testing.T) {
var _ logical.Storage = new(StorageClient)
}
func TestStorage_operations(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
storage := &logical.InmemStorage{}
server.RegisterName("Plugin", &StorageServer{
impl: storage,
})
testStorage := &StorageClient{client: client}
logical.TestStorage(t, testStorage)
}

247
logical/plugin/system.go Normal file
View File

@ -0,0 +1,247 @@
package plugin
import (
"net/rpc"
"time"
"fmt"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/helper/wrapping"
"github.com/hashicorp/vault/logical"
)
type SystemViewClient struct {
client *rpc.Client
}
func (s *SystemViewClient) DefaultLeaseTTL() time.Duration {
var reply DefaultLeaseTTLReply
err := s.client.Call("Plugin.DefaultLeaseTTL", new(interface{}), &reply)
if err != nil {
return 0
}
return reply.DefaultLeaseTTL
}
func (s *SystemViewClient) MaxLeaseTTL() time.Duration {
var reply MaxLeaseTTLReply
err := s.client.Call("Plugin.MaxLeaseTTL", new(interface{}), &reply)
if err != nil {
return 0
}
return reply.MaxLeaseTTL
}
func (s *SystemViewClient) SudoPrivilege(path string, token string) bool {
var reply SudoPrivilegeReply
args := &SudoPrivilegeArgs{
Path: path,
Token: token,
}
err := s.client.Call("Plugin.SudoPrivilege", args, &reply)
if err != nil {
return false
}
return reply.Sudo
}
func (s *SystemViewClient) Tainted() bool {
var reply TaintedReply
err := s.client.Call("Plugin.Tainted", new(interface{}), &reply)
if err != nil {
return false
}
return reply.Tainted
}
func (s *SystemViewClient) CachingDisabled() bool {
var reply CachingDisabledReply
err := s.client.Call("Plugin.CachingDisabled", new(interface{}), &reply)
if err != nil {
return false
}
return reply.CachingDisabled
}
func (s *SystemViewClient) ReplicationState() consts.ReplicationState {
var reply ReplicationStateReply
err := s.client.Call("Plugin.ReplicationState", new(interface{}), &reply)
if err != nil {
return consts.ReplicationDisabled
}
return reply.ReplicationState
}
func (s *SystemViewClient) ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) {
var reply ResponseWrapDataReply
// Do not allow JWTs to be returned
args := &ResponseWrapDataArgs{
Data: data,
TTL: ttl,
JWT: false,
}
err := s.client.Call("Plugin.ResponseWrapData", args, &reply)
if err != nil {
return nil, err
}
if reply.Error != nil {
return nil, reply.Error
}
return reply.ResponseWrapInfo, nil
}
func (s *SystemViewClient) LookupPlugin(name string) (*pluginutil.PluginRunner, error) {
return nil, fmt.Errorf("cannot call LookupPlugin from a plugin backend")
}
func (s *SystemViewClient) MlockEnabled() bool {
var reply MlockEnabledReply
err := s.client.Call("Plugin.MlockEnabled", new(interface{}), &reply)
if err != nil {
return false
}
return reply.MlockEnabled
}
type SystemViewServer struct {
impl logical.SystemView
}
func (s *SystemViewServer) DefaultLeaseTTL(_ interface{}, reply *DefaultLeaseTTLReply) error {
ttl := s.impl.DefaultLeaseTTL()
*reply = DefaultLeaseTTLReply{
DefaultLeaseTTL: ttl,
}
return nil
}
func (s *SystemViewServer) MaxLeaseTTL(_ interface{}, reply *MaxLeaseTTLReply) error {
ttl := s.impl.MaxLeaseTTL()
*reply = MaxLeaseTTLReply{
MaxLeaseTTL: ttl,
}
return nil
}
func (s *SystemViewServer) SudoPrivilege(args *SudoPrivilegeArgs, reply *SudoPrivilegeReply) error {
sudo := s.impl.SudoPrivilege(args.Path, args.Token)
*reply = SudoPrivilegeReply{
Sudo: sudo,
}
return nil
}
func (s *SystemViewServer) Tainted(_ interface{}, reply *TaintedReply) error {
tainted := s.impl.Tainted()
*reply = TaintedReply{
Tainted: tainted,
}
return nil
}
func (s *SystemViewServer) CachingDisabled(_ interface{}, reply *CachingDisabledReply) error {
cachingDisabled := s.impl.CachingDisabled()
*reply = CachingDisabledReply{
CachingDisabled: cachingDisabled,
}
return nil
}
func (s *SystemViewServer) ReplicationState(_ interface{}, reply *ReplicationStateReply) error {
replicationState := s.impl.ReplicationState()
*reply = ReplicationStateReply{
ReplicationState: replicationState,
}
return nil
}
func (s *SystemViewServer) ResponseWrapData(args *ResponseWrapDataArgs, reply *ResponseWrapDataReply) error {
// Do not allow JWTs to be returned
info, err := s.impl.ResponseWrapData(args.Data, args.TTL, false)
if err != nil {
*reply = ResponseWrapDataReply{
Error: plugin.NewBasicError(err),
}
return nil
}
*reply = ResponseWrapDataReply{
ResponseWrapInfo: info,
}
return nil
}
func (s *SystemViewServer) MlockEnabled(_ interface{}, reply *MlockEnabledReply) error {
enabled := s.impl.MlockEnabled()
*reply = MlockEnabledReply{
MlockEnabled: enabled,
}
return nil
}
type DefaultLeaseTTLReply struct {
DefaultLeaseTTL time.Duration
}
type MaxLeaseTTLReply struct {
MaxLeaseTTL time.Duration
}
type SudoPrivilegeArgs struct {
Path string
Token string
}
type SudoPrivilegeReply struct {
Sudo bool
}
type TaintedReply struct {
Tainted bool
}
type CachingDisabledReply struct {
CachingDisabled bool
}
type ReplicationStateReply struct {
ReplicationState consts.ReplicationState
}
type ResponseWrapDataArgs struct {
Data map[string]interface{}
TTL time.Duration
JWT bool
}
type ResponseWrapDataReply struct {
ResponseWrapInfo *wrapping.ResponseWrapInfo
Error *plugin.BasicError
}
type MlockEnabledReply struct {
MlockEnabled bool
}

View File

@ -0,0 +1,174 @@
package plugin
import (
"testing"
"reflect"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/logical"
)
func Test_impl(t *testing.T) {
var _ logical.SystemView = new(SystemViewClient)
}
func TestSystem_defaultLeaseTTL(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.DefaultLeaseTTL()
actual := testSystemView.DefaultLeaseTTL()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_maxLeaseTTL(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.MaxLeaseTTL()
actual := testSystemView.MaxLeaseTTL()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_sudoPrivilege(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
sys.SudoPrivilegeVal = true
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.SudoPrivilege("foo", "bar")
actual := testSystemView.SudoPrivilege("foo", "bar")
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_tainted(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
sys.TaintedVal = true
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.Tainted()
actual := testSystemView.Tainted()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_cachingDisabled(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
sys.CachingDisabledVal = true
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.CachingDisabled()
actual := testSystemView.CachingDisabled()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_replicationState(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
sys.ReplicationStateVal = consts.ReplicationPrimary
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.ReplicationState()
actual := testSystemView.ReplicationState()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}
func TestSystem_responseWrapData(t *testing.T) {
t.SkipNow()
}
func TestSystem_lookupPlugin(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
if _, err := testSystemView.LookupPlugin("foo"); err == nil {
t.Fatal("LookPlugin(): expected error on due to unsupported call from plugin")
}
}
func TestSystem_mlockEnabled(t *testing.T) {
client, server := plugin.TestRPCConn(t)
defer client.Close()
sys := logical.TestSystemView()
sys.EnableMlock = true
server.RegisterName("Plugin", &SystemViewServer{
impl: sys,
})
testSystemView := &SystemViewClient{client: client}
expected := sys.MlockEnabled()
actual := testSystemView.MlockEnabled()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected: %v, got: %v", expected, actual)
}
}

View File

@ -95,9 +95,13 @@ func (c *Core) enableCredential(entry *MountEntry) error {
viewPath := credentialBarrierPrefix + entry.UUID + "/"
view := NewBarrierView(c.barrier, viewPath)
sysView := c.mountEntrySysView(entry)
conf := make(map[string]string)
if entry.Config.PluginName != "" {
conf["plugin_name"] = entry.Config.PluginName
}
// Create the new backend
backend, err := c.newCredentialBackend(entry.Type, sysView, view, nil)
backend, err := c.newCredentialBackend(entry.Type, sysView, view, conf)
if err != nil {
return err
}
@ -105,6 +109,12 @@ func (c *Core) enableCredential(entry *MountEntry) error {
return fmt.Errorf("nil backend returned from %q factory", entry.Type)
}
// Check for the correct backend type
backendType := backend.Type()
if entry.Type == "plugin" && backendType != logical.TypeCredential {
return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backendType)
}
if err := backend.Initialize(); err != nil {
return err
}
@ -406,9 +416,13 @@ func (c *Core) setupCredentials() error {
viewPath := credentialBarrierPrefix + entry.UUID + "/"
view = NewBarrierView(c.barrier, viewPath)
sysView := c.mountEntrySysView(entry)
conf := make(map[string]string)
if entry.Config.PluginName != "" {
conf["plugin_name"] = entry.Config.PluginName
}
// Initialize the backend
backend, err = c.newCredentialBackend(entry.Type, sysView, view, nil)
backend, err = c.newCredentialBackend(entry.Type, sysView, view, conf)
if err != nil {
c.logger.Error("core: failed to create credential entry", "path", entry.Path, "error", err)
return errLoadAuthFailed
@ -417,6 +431,12 @@ func (c *Core) setupCredentials() error {
return fmt.Errorf("nil backend returned from %q factory", entry.Type)
}
// Check for the correct backend type
backendType := backend.Type()
if entry.Type == "plugin" && backendType != logical.TypeCredential {
return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backendType)
}
if err := backend.Initialize(); err != nil {
return err
}

View File

@ -516,7 +516,10 @@ func NewCore(conf *CoreConfig) (*Core, error) {
logicalBackends["cubbyhole"] = CubbyholeBackendFactory
logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) {
b := NewSystemBackend(c)
return b.Backend.Setup(config)
if err := b.Setup(config); err != nil {
return nil, err
}
return b, nil
}
c.logicalBackends = logicalBackends

View File

@ -1459,5 +1459,10 @@ func badRenewFactory(conf *logical.BackendConfig) (logical.Backend, error) {
},
}
return be.Setup(conf)
err := be.Setup(conf)
if err != nil {
return nil, err
}
return be, nil
}

View File

@ -17,13 +17,13 @@ func PassthroughBackendFactory(conf *logical.BackendConfig) (logical.Backend, er
return LeaseSwitchedPassthroughBackend(conf, false)
}
// PassthroughBackendWithLeasesFactory returns a PassthroughBackend
// LeasedPassthroughBackendFactory returns a PassthroughBackend
// with leases switched on
func LeasedPassthroughBackendFactory(conf *logical.BackendConfig) (logical.Backend, error) {
return LeaseSwitchedPassthroughBackend(conf, true)
}
// LeaseSwitchedPassthroughBackendFactory returns a PassthroughBackend
// LeaseSwitchedPassthroughBackend returns a PassthroughBackend
// with leases switched on or off
func LeaseSwitchedPassthroughBackend(conf *logical.BackendConfig, leases bool) (logical.Backend, error) {
var b PassthroughBackend
@ -147,6 +147,10 @@ func (b *PassthroughBackend) handleRead(
return resp, nil
}
func (b *PassthroughBackend) GeneratesLeases() bool {
return b.generateLeases
}
func (b *PassthroughBackend) handleWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Check that some fields are given
@ -202,10 +206,6 @@ func (b *PassthroughBackend) handleList(
return logical.ListResponse(keys), nil
}
func (b *PassthroughBackend) GeneratesLeases() bool {
return b.generateLeases
}
const passthroughHelp = `
The generic backend reads and writes arbitrary secrets to the backend.
The secrets are encrypted/decrypted by Vault: they are never stored

View File

@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/helper/wrapping"
@ -486,6 +487,10 @@ func NewSystemBackend(core *Core) *SystemBackend {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["auth_desc"][0]),
},
"plugin_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["auth_plugin"][0]),
},
"local": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
@ -775,7 +780,7 @@ func NewSystemBackend(core *Core) *SystemBackend {
HelpDescription: strings.TrimSpace(sysHelp["audited-headers"][1]),
},
&framework.Path{
Pattern: "plugins/catalog/$",
Pattern: "plugins/catalog/?$",
Fields: map[string]*framework.FieldSchema{},
@ -792,18 +797,15 @@ func NewSystemBackend(core *Core) *SystemBackend {
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The name of the plugin",
Description: strings.TrimSpace(sysHelp["plugin-catalog_name"][0]),
},
"sha_256": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The SHA256 sum of the executable used in the
command field. This should be HEX encoded.`,
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-catalog_sha-256"][0]),
},
"command": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The command used to start the plugin. The
executable defined in this command must exist in vault's
plugin directory.`,
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-catalog_command"][0]),
},
},
@ -943,10 +945,11 @@ func (b *SystemBackend) handlePluginCatalogRead(req *logical.Request, d *framewo
return nil, nil
}
// Create a map of data to be returned and remove sensitive information from it
data := structs.New(plugin).Map()
return &logical.Response{
Data: map[string]interface{}{
"plugin": plugin,
},
Data: data,
}, nil
}
@ -1157,18 +1160,17 @@ func (b *SystemBackend) handleMountTable(
}
for _, entry := range b.Core.mounts.Entries {
// Populate mount info
structConfig := structs.New(entry.Config).Map()
structConfig["default_lease_ttl"] = int64(structConfig["default_lease_ttl"].(time.Duration).Seconds())
structConfig["max_lease_ttl"] = int64(structConfig["max_lease_ttl"].(time.Duration).Seconds())
info := map[string]interface{}{
"type": entry.Type,
"description": entry.Description,
"accessor": entry.Accessor,
"config": map[string]interface{}{
"default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()),
"max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()),
"force_no_cache": entry.Config.ForceNoCache,
},
"local": entry.Local,
"config": structConfig,
"local": entry.Local,
}
resp.Data[entry.Path] = info
}
@ -1195,12 +1197,8 @@ func (b *SystemBackend) handleMount(
path = sanitizeMountPath(path)
var config MountConfig
var apiConfig APIMountConfig
var apiConfig struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
}
configMap := data.Get("config").(map[string]interface{})
if configMap != nil && len(configMap) != 0 {
err := mapstructure.Decode(configMap, &apiConfig)
@ -1249,6 +1247,11 @@ func (b *SystemBackend) handleMount(
logical.ErrInvalidRequest
}
// Only set plugin-name if mount is of type plugin
if logicalType == "plugin" && apiConfig.PluginName != "" {
config.PluginName = apiConfig.PluginName
}
// Copy over the force no cache if set
if apiConfig.ForceNoCache {
config.ForceNoCache = true
@ -1685,6 +1688,14 @@ func (b *SystemBackend) handleEnableAuth(
path := data.Get("path").(string)
logicalType := data.Get("type").(string)
description := data.Get("description").(string)
pluginName := data.Get("plugin_name").(string)
var config MountConfig
// Only set plugin name if mount is of type plugin
if logicalType == "plugin" && pluginName != "" {
config.PluginName = pluginName
}
if logicalType == "" {
return logical.ErrorResponse(
@ -1700,6 +1711,7 @@ func (b *SystemBackend) handleEnableAuth(
Path: path,
Type: logicalType,
Description: description,
Config: config,
Local: local,
}
@ -2585,6 +2597,11 @@ Example: you might have an OAuth backend for GitHub, and one for Google Apps.
"",
},
"auth_plugin": {
`Name of the auth plugin to use based from the name in the plugin catalog.`,
"",
},
"policy-list": {
`List the configured access control policies.`,
`
@ -2764,23 +2781,38 @@ This path responds to the following HTTP methods.
"Lists the headers configured to be audited.",
`Returns a list of headers that have been configured to be audited.`,
},
"plugins/catalog": {
`Configures the plugins known to vault`,
"plugin-catalog": {
"Configures the plugins known to vault",
`
This path responds to the following HTTP methods.
LIST /
Returns a list of names of configured plugins.
LIST /
Returns a list of names of configured plugins.
GET /<name>
Retrieve the metadata for the named plugin.
GET /<name>
Retrieve the metadata for the named plugin.
PUT /<name>
Add or update plugin.
PUT /<name>
Add or update plugin.
DELETE /<name>
Delete the plugin with the given name.
DELETE /<name>
Delete the plugin with the given name.
`,
},
"plugin-catalog_name": {
"The name of the plugin",
"",
},
"plugin-catalog_sha-256": {
`The SHA256 sum of the executable used in the
command field. This should be HEX encoded.`,
"",
},
"plugin-catalog_command": {
`The command used to start the plugin. The
executable defined in this command must exist in vault's
plugin directory.`,
"",
},
"leases": {
`View or list lease metadata.`,
`

View File

@ -0,0 +1,104 @@
package vault_test
import (
"io/ioutil"
"os"
"testing"
"time"
"github.com/hashicorp/vault/builtin/plugin"
"github.com/hashicorp/vault/helper/logformat"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
lplugin "github.com/hashicorp/vault/logical/plugin"
"github.com/hashicorp/vault/logical/plugin/mock"
"github.com/hashicorp/vault/vault"
log "github.com/mgutz/logxi/v1"
)
func TestSystemBackend_enableAuth_plugin(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"plugin": plugin.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, true)
cluster.StartListeners()
defer cluster.CloseListeners()
cores := cluster.Cores
cores[0].Handler.Handle("/", http.Handler(cores[0].Core))
cores[1].Handler.Handle("/", http.Handler(cores[1].Core))
cores[2].Handler.Handle("/", http.Handler(cores[2].Core))
core := cores[0]
b := vault.NewSystemBackend(core.Core)
logger := logformat.NewVaultLogger(log.LevelTrace)
bc := &logical.BackendConfig{
Logger: logger,
System: logical.StaticSystemView{
DefaultLeaseTTLVal: time.Hour * 24,
MaxLeaseTTLVal: time.Hour * 24 * 32,
},
}
err := b.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain")
req := logical.TestRequest(t, logical.UpdateOperation, "auth/mock-plugin")
req.Data["type"] = "plugin"
req.Data["plugin_name"] = "mock-plugin"
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestBackend_PluginMain(t *testing.T) {
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
return
}
content := []byte(vault.TestClusterCACert)
tmpfile, err := ioutil.TempFile("", "test-cacert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
factoryFunc := mock.FactoryType(logical.TypeCredential)
args := []string{"--ca-cert=" + tmpfile.Name()}
apiClientMeta := &pluginutil.APIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(args)
tlsConfig := apiClientMeta.GetTLSConfig()
tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig)
err = lplugin.Serve(&lplugin.ServeOpts{
BackendFactoryFunc: factoryFunc,
TLSProviderFunc: tlsProviderFunc,
})
if err != nil {
t.Fatal(err)
}
}

View File

@ -985,8 +985,8 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32,
},
}
be := NewSystemBackend(core)
b, err := be.Backend.Setup(bc)
b := NewSystemBackend(core)
err := b.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}
@ -1049,8 +1049,8 @@ func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32,
},
}
be := NewSystemBackend(core)
b, err := be.Backend.Setup(bc)
b := NewSystemBackend(core)
err := b.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}
@ -1591,7 +1591,7 @@ func testSystemBackend(t *testing.T) logical.Backend {
}
b := NewSystemBackend(c)
_, err := b.Backend.Setup(bc)
err := b.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}
@ -1610,7 +1610,7 @@ func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) {
}
b := NewSystemBackend(c)
_, err := b.Backend.Setup(bc)
err := b.Backend.Setup(bc)
if err != nil {
t.Fatal(err)
}
@ -1641,22 +1641,16 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
if err != nil {
t.Fatalf("err: %v", err)
}
actualRespData := resp.Data
expectedBuiltin := &pluginutil.PluginRunner{
Name: "mysql-database-plugin",
Builtin: true,
}
expectedBuiltin.BuiltinFactory, _ = builtinplugins.Get("mysql-database-plugin")
expectedRespData := structs.New(expectedBuiltin).Map()
p := resp.Data["plugin"].(*pluginutil.PluginRunner)
if &(p.BuiltinFactory) == &(expectedBuiltin.BuiltinFactory) {
t.Fatal("expected BuiltinFactory did not match actual")
}
expectedBuiltin.BuiltinFactory = nil
p.BuiltinFactory = nil
if !reflect.DeepEqual(p, expectedBuiltin) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", resp.Data["plugin"].(*pluginutil.PluginRunner), expectedBuiltin)
if !reflect.DeepEqual(actualRespData, expectedRespData) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actualRespData, expectedRespData)
}
// Set a plugin
@ -1680,16 +1674,19 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
if err != nil {
t.Fatalf("err: %v", err)
}
actual := resp.Data
expected := &pluginutil.PluginRunner{
expectedRunner := &pluginutil.PluginRunner{
Name: "test-plugin",
Command: filepath.Join(sym, filepath.Base(file.Name())),
Args: []string{"--test"},
Sha256: []byte{'1'},
Builtin: false,
}
if !reflect.DeepEqual(resp.Data["plugin"].(*pluginutil.PluginRunner), expected) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", resp.Data["plugin"].(*pluginutil.PluginRunner), expected)
expected := structs.New(expectedRunner).Map()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected)
}
// Delete plugin

View File

@ -167,6 +167,15 @@ type MountConfig struct {
DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default
MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
// APIMountConfig is an embedded struct of api.MountConfigInput
type APIMountConfig struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
// Mount is used to mount a new backend to the mount table.
@ -216,8 +225,13 @@ func (c *Core) mount(entry *MountEntry) error {
viewPath := backendBarrierPrefix + entry.UUID + "/"
view := NewBarrierView(c.barrier, viewPath)
sysView := c.mountEntrySysView(entry)
conf := make(map[string]string)
if entry.Config.PluginName != "" {
conf["plugin_name"] = entry.Config.PluginName
}
backend, err := c.newLogicalBackend(entry.Type, sysView, view, nil)
// Consider having plugin name under entry.Options
backend, err := c.newLogicalBackend(entry.Type, sysView, view, conf)
if err != nil {
return err
}
@ -225,8 +239,14 @@ func (c *Core) mount(entry *MountEntry) error {
return fmt.Errorf("nil backend of type %q returned from creation function", entry.Type)
}
// Check for the correct backend type
backendType := backend.Type()
if entry.Type == "plugin" && backendType != logical.TypeLogical {
return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backendType)
}
// Call initialize; this takes care of init tasks that must be run after
// the ignore paths are collected
// the ignore paths are collected.
if err := backend.Initialize(); err != nil {
return err
}
@ -658,9 +678,13 @@ func (c *Core) setupMounts() error {
// Create a barrier view using the UUID
view = NewBarrierView(c.barrier, barrierPath)
sysView := c.mountEntrySysView(entry)
// Initialize the backend
// Set up conf to pass in plugin_name
conf := make(map[string]string)
if entry.Config.PluginName != "" {
conf["plugin_name"] = entry.Config.PluginName
}
// Create the new backend
backend, err = c.newLogicalBackend(entry.Type, sysView, view, nil)
backend, err = c.newLogicalBackend(entry.Type, sysView, view, conf)
if err != nil {
c.logger.Error("core: failed to create mount entry", "path", entry.Path, "error", err)
return errLoadMountsFailed
@ -669,6 +693,12 @@ func (c *Core) setupMounts() error {
return fmt.Errorf("created mount entry of type %q is nil", entry.Type)
}
// Check for the correct backend type
backendType := backend.Type()
if entry.Type == "plugin" && backendType != logical.TypeLogical {
return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backendType)
}
if err := backend.Initialize(); err != nil {
return err
}
@ -687,10 +717,9 @@ func (c *Core) setupMounts() error {
if err != nil {
c.logger.Error("core: failed to mount entry", "path", entry.Path, "error", err)
return errLoadMountsFailed
} else {
if c.logger.IsInfo() {
c.logger.Info("core: successfully mounted backend", "type", entry.Type, "path", entry.Path)
}
}
if c.logger.IsInfo() {
c.logger.Info("core: successfully mounted backend", "type", entry.Type, "path", entry.Path)
}
// Ensure the path is tainted if set in the mount table

View File

@ -2,6 +2,7 @@ package vault
import (
"fmt"
"io/ioutil"
"reflect"
"strings"
"sync"
@ -9,7 +10,9 @@ import (
"time"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/logformat"
"github.com/hashicorp/vault/logical"
log "github.com/mgutz/logxi/v1"
)
type NoopBackend struct {
@ -63,10 +66,26 @@ func (n *NoopBackend) InvalidateKey(k string) {
n.Invalidations = append(n.Invalidations, k)
}
func (n *NoopBackend) Setup(config *logical.BackendConfig) error {
return nil
}
func (n *NoopBackend) Logger() log.Logger {
return logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelOff)
}
func (n *NoopBackend) Initialize() error {
return nil
}
func (n *NoopBackend) Type() logical.BackendType {
return logical.TypeLogical
}
func (n *NoopBackend) RegisterLicense(license interface{}) error {
return nil
}
func TestRouter_Mount(t *testing.T) {
r := NewRouter()
_, barrier, _ := mockBarrier(t)

View File

@ -520,6 +520,10 @@ func (n *rawHTTP) System() logical.SystemView {
}
}
func (n *rawHTTP) Logger() log.Logger {
return logformat.NewVaultLogger(log.LevelTrace)
}
func (n *rawHTTP) Cleanup() {
// noop
}
@ -533,6 +537,19 @@ func (n *rawHTTP) InvalidateKey(string) {
// noop
}
func (n *rawHTTP) Setup(config *logical.BackendConfig) error {
// noop
return nil
}
func (n *rawHTTP) Type() logical.BackendType {
return logical.TypeUnknown
}
func (n *rawHTTP) RegisterLicense(license interface{}) error {
return nil
}
func GenerateRandBytes(length int) ([]byte, error) {
if length < 0 {
return nil, fmt.Errorf("length must be >= 0")

View File

@ -0,0 +1,44 @@
---
layout: "docs"
page_title: "Plugin Backends"
sidebar_current: "docs-plugin"
description: |-
Plugin backends are mountable backends that are implemented unsing Vault's plugin system.
---
# Plugin Backends
Plugin backends are the components in Vault that can be implemented separately from Vault's
builtin backends. These backends can be either authentication or secret backends.
Detailed information regarding the plugin system can be found in the
[internals documentation](https://www.vaultproject.io/docs/internals/plugins.html).
# Mounting/unmounting Plugin Backends
Before a plugin backend can be mounted, it needs to be registered via the
[plugin catalog](https://www.vaultproject.io/docs/internals/plugins.html#plugin-catalog). After
the plugin is registered, it can be mounted by specifying the registered plugin name:
```
$ vault mount -path=my-secrets -plugin-name=passthrough-plugin plugin
Successfully mounted plugin 'passthrough-plugin' at 'my-secrets'!
```
Listing mounts will display backends that are mounted as plugins, along with the
name of plugin backend that is mounted:
```
$ vault mounts
Path Type Accessor Plugin Default TTL Max TTL Force No Cache Replication Behavior Description
my-secrets/ plugin plugin_deb84140 passthrough-plugin system system false replicated
...
```
Unmounting a plugin backend is the identical to unmounting internal backends:
```
$ vault unmount my-secrets
```

View File

@ -316,6 +316,10 @@
</ul>
</li>
<li<%= sidebar_current("docs-plugin") %>>
<a href="/docs/plugin/index.html">Plugin Backends</a>
</li>
<hr>
<li<%= sidebar_current("docs-vault-enterprise") %>>