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

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)
}
}