Move plugin code into sub directory

This commit is contained in:
Brian Kassouf 2017-04-06 12:20:10 -07:00
parent 2e23cf58b8
commit 62d59e5f4e
10 changed files with 385 additions and 365 deletions

View File

@ -4,50 +4,23 @@ import (
"fmt"
"strings"
"sync"
"time"
log "github.com/mgutz/logxi/v1"
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const databaseConfigPath = "database/dbs/"
// DatabaseType is the interface that all database objects must implement.
type DatabaseType interface {
Type() string
CreateUser(statements Statements, username, password, expiration string) error
RenewUser(statements Statements, username, expiration string) error
RevokeUser(statements Statements, username string) error
Initialize(map[string]interface{}) error
Close() error
GenerateUsername(displayName string) (string, error)
GeneratePassword() (string, error)
GenerateExpiration(ttl time.Duration) (string, error)
}
// DatabaseConfig is used by the Factory function to configure a DatabaseType
// object.
type DatabaseConfig struct {
PluginName string `json:"plugin_name" structs:"plugin_name" mapstructure:"plugin_name"`
// ConnectionDetails stores the database specific connection settings needed
// by each database type.
ConnectionDetails map[string]interface{} `json:"connection_details" structs:"connection_details" mapstructure:"connection_details"`
MaxOpenConnections int `json:"max_open_connections" structs:"max_open_connections" mapstructure:"max_open_connections"`
MaxIdleConnections int `json:"max_idle_connections" structs:"max_idle_connections" mapstructure:"max_idle_connections"`
MaxConnectionLifetime time.Duration `json:"max_connection_lifetime" structs:"max_connection_lifetime" mapstructure:"max_connection_lifetime"`
}
// Statements set in role creation and passed into the database type's functions.
// TODO: Add a way of setting defaults here.
type Statements struct {
CreationStatements string `json:"creation_statments" mapstructure:"creation_statements" structs:"creation_statments"`
RevocationStatements string `json:"revocation_statements" mapstructure:"revocation_statements" structs:"revocation_statements"`
RollbackStatements string `json:"rollback_statements" mapstructure:"rollback_statements" structs:"rollback_statements"`
RenewStatements string `json:"renew_statements" mapstructure:"renew_statements" structs:"renew_statements"`
ConnectionDetails map[string]interface{} `json:"connection_details" structs:"connection_details" mapstructure:"connection_details"`
}
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
@ -83,12 +56,12 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
}
b.logger = conf.Logger
b.connections = make(map[string]DatabaseType)
b.connections = make(map[string]dbplugin.DatabaseType)
return &b
}
type databaseBackend struct {
connections map[string]DatabaseType
connections map[string]dbplugin.DatabaseType
logger log.Logger
*framework.Backend
@ -108,7 +81,7 @@ func (b *databaseBackend) closeAllDBs() {
// This function is used to retrieve a database object either from the cached
// connection map or by using the database config in storage. The caller of this
// function needs to hold the backend's lock.
func (b *databaseBackend) getOrCreateDBObj(s logical.Storage, name string) (DatabaseType, error) {
func (b *databaseBackend) getOrCreateDBObj(s logical.Storage, name string) (dbplugin.DatabaseType, error) {
// if the object already is built and cached, return it
db, ok := b.connections[name]
if ok {
@ -128,7 +101,7 @@ func (b *databaseBackend) getOrCreateDBObj(s logical.Storage, name string) (Data
return nil, err
}
db, err = PluginFactory(&config, b.System(), b.logger)
db, err = dbplugin.PluginFactory(config.PluginName, b.System(), b.logger)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,148 @@
package dbplugin
import (
"fmt"
"net/rpc"
"sync"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
)
// DatabasePluginClient embeds a databasePluginRPCClient and wraps it's close
// method to also call Kill() on the plugin.Client.
type DatabasePluginClient struct {
client *plugin.Client
sync.Mutex
*databasePluginRPCClient
}
func (dc *DatabasePluginClient) Close() error {
err := dc.databasePluginRPCClient.Close()
dc.client.Kill()
return err
}
// newPluginClient returns a databaseRPCClient with a connection to a running
// plugin. The client is wrapped in a DatabasePluginClient object to ensure the
// plugin is killed on call of Close().
func newPluginClient(sys pluginutil.Wrapper, pluginRunner *pluginutil.PluginRunner) (DatabaseType, error) {
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"database": new(DatabasePlugin),
}
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("database")
if err != nil {
return nil, err
}
// We should have a Greeter now! This feels like a normal interface
// implementation but is in fact over an RPC connection.
databaseRPC := raw.(*databasePluginRPCClient)
return &DatabasePluginClient{
client: client,
databasePluginRPCClient: databaseRPC,
}, nil
}
// ---- RPC client domain ----
// databasePluginRPCClient impliments DatabaseType and is used on the client to
// make RPC calls to a plugin.
type databasePluginRPCClient struct {
client *rpc.Client
}
func (dr *databasePluginRPCClient) Type() string {
var dbType string
//TODO: catch error
dr.client.Call("Plugin.Type", struct{}{}, &dbType)
return fmt.Sprintf("plugin-%s", dbType)
}
func (dr *databasePluginRPCClient) CreateUser(statements Statements, username, password, expiration string) error {
req := CreateUserRequest{
Statements: statements,
Username: username,
Password: password,
Expiration: expiration,
}
err := dr.client.Call("Plugin.CreateUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) RenewUser(statements Statements, username, expiration string) error {
req := RenewUserRequest{
Statements: statements,
Username: username,
Expiration: expiration,
}
err := dr.client.Call("Plugin.RenewUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) RevokeUser(statements Statements, username string) error {
req := RevokeUserRequest{
Statements: statements,
Username: username,
}
err := dr.client.Call("Plugin.RevokeUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) Initialize(conf map[string]interface{}) error {
err := dr.client.Call("Plugin.Initialize", conf, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) Close() error {
err := dr.client.Call("Plugin.Close", struct{}{}, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) GenerateUsername(displayName string) (string, error) {
resp := &GenerateUsernameResponse{}
err := dr.client.Call("Plugin.GenerateUsername", displayName, resp)
return resp.Username, err
}
func (dr *databasePluginRPCClient) GeneratePassword() (string, error) {
resp := &GeneratePasswordResponse{}
err := dr.client.Call("Plugin.GeneratePassword", struct{}{}, resp)
return resp.Password, err
}
func (dr *databasePluginRPCClient) GenerateExpiration(duration time.Duration) (string, error) {
resp := &GenerateExpirationResponse{}
err := dr.client.Call("Plugin.GenerateExpiration", duration, resp)
return resp.Expiration, err
}

View File

@ -1,4 +1,4 @@
package database
package dbplugin
import (
"time"

View File

@ -0,0 +1,126 @@
package dbplugin
import (
"errors"
"net/rpc"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
log "github.com/mgutz/logxi/v1"
)
var (
ErrEmptyPluginName = errors.New("empty plugin name")
)
// DatabaseType is the interface that all database objects must implement.
type DatabaseType interface {
Type() string
CreateUser(statements Statements, username, password, expiration string) error
RenewUser(statements Statements, username, expiration string) error
RevokeUser(statements Statements, username string) error
Initialize(map[string]interface{}) error
Close() error
GenerateUsername(displayName string) (string, error)
GeneratePassword() (string, error)
GenerateExpiration(ttl time.Duration) (string, error)
}
// Statements set in role creation and passed into the database type's functions.
// TODO: Add a way of setting defaults here.
type Statements struct {
CreationStatements string `json:"creation_statments" mapstructure:"creation_statements" structs:"creation_statments"`
RevocationStatements string `json:"revocation_statements" mapstructure:"revocation_statements" structs:"revocation_statements"`
RollbackStatements string `json:"rollback_statements" mapstructure:"rollback_statements" structs:"rollback_statements"`
RenewStatements string `json:"renew_statements" mapstructure:"renew_statements" structs:"renew_statements"`
}
// PluginFactory is used to build plugin database types. It wraps the database
// object in a logging and metrics middleware.
func PluginFactory(pluginName string, sys pluginutil.LookWrapper, logger log.Logger) (DatabaseType, error) {
if pluginName == "" {
return nil, ErrEmptyPluginName
}
pluginMeta, err := sys.LookupPlugin(pluginName)
if err != nil {
return nil, err
}
db, err := newPluginClient(sys, pluginMeta)
if err != nil {
return nil, err
}
// Wrap with metrics middleware
db = &databaseMetricsMiddleware{
next: db,
typeStr: db.Type(),
}
// Wrap with tracing middleware
db = &databaseTracingMiddleware{
next: db,
typeStr: db.Type(),
logger: logger,
}
return db, 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_DATABASE_PLUGIN",
MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb",
}
type DatabasePlugin struct {
impl DatabaseType
}
func (d DatabasePlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &databasePluginRPCServer{impl: d.impl}, nil
}
func (DatabasePlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &databasePluginRPCClient{client: c}, nil
}
// ---- RPC Request Args Domain ----
type CreateUserRequest struct {
Statements Statements
Username string
Password string
Expiration string
}
type RenewUserRequest struct {
Statements Statements
Username string
Expiration string
}
type RevokeUserRequest struct {
Statements Statements
Username string
}
// ---- RPC Response Args Domain ----
type GenerateUsernameResponse struct {
Username string
}
type GenerateExpirationResponse struct {
Expiration string
}
type GeneratePasswordResponse struct {
Password string
}

View File

@ -1,4 +1,4 @@
package database
package dbplugin
import (
"crypto/sha256"

View File

@ -0,0 +1,90 @@
package dbplugin
import (
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
)
// NewPluginServer is called from within a plugin and wraps the provided
// DatabaseType implimentation in a databasePluginRPCServer object and starts a
// RPC server.
func NewPluginServer(db DatabaseType) {
dbPlugin := &DatabasePlugin{
impl: db,
}
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"database": dbPlugin,
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
TLSProvider: pluginutil.VaultPluginTLSProvider,
})
}
// ---- RPC server domain ----
// databasePluginRPCServer impliments DatabaseType and is run inside a plugin
type databasePluginRPCServer struct {
impl DatabaseType
}
func (ds *databasePluginRPCServer) Type(_ struct{}, resp *string) error {
*resp = ds.impl.Type()
return nil
}
func (ds *databasePluginRPCServer) CreateUser(args *CreateUserRequest, _ *struct{}) error {
err := ds.impl.CreateUser(args.Statements, args.Username, args.Password, args.Expiration)
return err
}
func (ds *databasePluginRPCServer) RenewUser(args *RenewUserRequest, _ *struct{}) error {
err := ds.impl.RenewUser(args.Statements, args.Username, args.Expiration)
return err
}
func (ds *databasePluginRPCServer) RevokeUser(args *RevokeUserRequest, _ *struct{}) error {
err := ds.impl.RevokeUser(args.Statements, args.Username)
return err
}
func (ds *databasePluginRPCServer) Initialize(args map[string]interface{}, _ *struct{}) error {
err := ds.impl.Initialize(args)
return err
}
func (ds *databasePluginRPCServer) Close(_ struct{}, _ *struct{}) error {
ds.impl.Close()
return nil
}
func (ds *databasePluginRPCServer) GenerateUsername(args string, resp *GenerateUsernameResponse) error {
var err error
resp.Username, err = ds.impl.GenerateUsername(args)
return err
}
func (ds *databasePluginRPCServer) GeneratePassword(_ struct{}, resp *GeneratePasswordResponse) error {
var err error
resp.Password, err = ds.impl.GeneratePassword()
return err
}
func (ds *databasePluginRPCServer) GenerateExpiration(args time.Duration, resp *GenerateExpirationResponse) error {
var err error
resp.Expiration, err = ds.impl.GenerateExpiration(args)
return err
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/fatih/structs"
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -163,7 +164,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
b.Lock()
defer b.Unlock()
db, err := PluginFactory(config, b.System(), b.logger)
db, err := dbplugin.PluginFactory(config.PluginName, b.System(), b.logger)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error creating database object: %s", err)), nil
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -155,7 +156,7 @@ func (b *databaseBackend) pathRoleCreate(req *logical.Request, data *framework.F
"Invalid max_ttl: %s", err)), nil
}
statements := Statements{
statements := dbplugin.Statements{
CreationStatements: creationStmts,
RevocationStatements: revocationStmts,
RollbackStatements: rollbackStmts,
@ -182,10 +183,10 @@ func (b *databaseBackend) pathRoleCreate(req *logical.Request, data *framework.F
}
type roleEntry struct {
DBName string `json:"db_name" mapstructure:"db_name" structs:"db_name"`
Statements Statements `json:"statments" mapstructure:"statements" structs:"statments"`
DefaultTTL time.Duration `json:"default_ttl" mapstructure:"default_ttl" structs:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl" mapstructure:"max_ttl" structs:"max_ttl"`
DBName string `json:"db_name" mapstructure:"db_name" structs:"db_name"`
Statements dbplugin.Statements `json:"statments" mapstructure:"statements" structs:"statments"`
DefaultTTL time.Duration `json:"default_ttl" mapstructure:"default_ttl" structs:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl" mapstructure:"max_ttl" structs:"max_ttl"`
}
const pathRoleHelpSyn = `

View File

@ -1,324 +0,0 @@
package database
import (
"errors"
"fmt"
"net/rpc"
"sync"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/logical"
log "github.com/mgutz/logxi/v1"
)
var (
ErrEmptyPluginName = errors.New("empty plugin name")
)
// PluginFactory is used to build plugin database types. It wraps the database
// object in a logging and metrics middleware.
func PluginFactory(conf *DatabaseConfig, sys logical.SystemView, logger log.Logger) (DatabaseType, error) {
if conf.PluginName == "" {
return nil, ErrEmptyPluginName
}
pluginMeta, err := sys.LookupPlugin(conf.PluginName)
if err != nil {
return nil, err
}
db, err := newPluginClient(sys, pluginMeta)
if err != nil {
return nil, err
}
// Wrap with metrics middleware
db = &databaseMetricsMiddleware{
next: db,
typeStr: db.Type(),
}
// Wrap with tracing middleware
db = &databaseTracingMiddleware{
next: db,
typeStr: db.Type(),
logger: logger,
}
return db, 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_DATABASE_PLUGIN",
MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb",
}
type DatabasePlugin struct {
impl DatabaseType
}
func (d DatabasePlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &databasePluginRPCServer{impl: d.impl}, nil
}
func (DatabasePlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &databasePluginRPCClient{client: c}, nil
}
// DatabasePluginClient embeds a databasePluginRPCClient and wraps it's close
// method to also call Kill() on the plugin.Client.
type DatabasePluginClient struct {
client *plugin.Client
sync.Mutex
*databasePluginRPCClient
}
func (dc *DatabasePluginClient) Close() error {
err := dc.databasePluginRPCClient.Close()
dc.client.Kill()
return err
}
// newPluginClient returns a databaseRPCClient with a connection to a running
// plugin. The client is wrapped in a DatabasePluginClient object to ensure the
// plugin is killed on call of Close().
func newPluginClient(sys pluginutil.Wrapper, pluginRunner *pluginutil.PluginRunner) (DatabaseType, error) {
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"database": new(DatabasePlugin),
}
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("database")
if err != nil {
return nil, err
}
// We should have a Greeter now! This feels like a normal interface
// implementation but is in fact over an RPC connection.
databaseRPC := raw.(*databasePluginRPCClient)
return &DatabasePluginClient{
client: client,
databasePluginRPCClient: databaseRPC,
}, nil
}
// NewPluginServer is called from within a plugin and wraps the provided
// DatabaseType implimentation in a databasePluginRPCServer object and starts a
// RPC server.
func NewPluginServer(db DatabaseType) {
dbPlugin := &DatabasePlugin{
impl: db,
}
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"database": dbPlugin,
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
TLSProvider: pluginutil.VaultPluginTLSProvider,
})
}
// ---- RPC client domain ----
// databasePluginRPCClient impliments DatabaseType and is used on the client to
// make RPC calls to a plugin.
type databasePluginRPCClient struct {
client *rpc.Client
}
func (dr *databasePluginRPCClient) Type() string {
var dbType string
//TODO: catch error
dr.client.Call("Plugin.Type", struct{}{}, &dbType)
return fmt.Sprintf("plugin-%s", dbType)
}
func (dr *databasePluginRPCClient) CreateUser(statements Statements, username, password, expiration string) error {
req := CreateUserRequest{
Statements: statements,
Username: username,
Password: password,
Expiration: expiration,
}
err := dr.client.Call("Plugin.CreateUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) RenewUser(statements Statements, username, expiration string) error {
req := RenewUserRequest{
Statements: statements,
Username: username,
Expiration: expiration,
}
err := dr.client.Call("Plugin.RenewUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) RevokeUser(statements Statements, username string) error {
req := RevokeUserRequest{
Statements: statements,
Username: username,
}
err := dr.client.Call("Plugin.RevokeUser", req, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) Initialize(conf map[string]interface{}) error {
err := dr.client.Call("Plugin.Initialize", conf, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) Close() error {
err := dr.client.Call("Plugin.Close", struct{}{}, &struct{}{})
return err
}
func (dr *databasePluginRPCClient) GenerateUsername(displayName string) (string, error) {
resp := &GenerateUsernameResponse{}
err := dr.client.Call("Plugin.GenerateUsername", displayName, resp)
return resp.Username, err
}
func (dr *databasePluginRPCClient) GeneratePassword() (string, error) {
resp := &GeneratePasswordResponse{}
err := dr.client.Call("Plugin.GeneratePassword", struct{}{}, resp)
return resp.Password, err
}
func (dr *databasePluginRPCClient) GenerateExpiration(duration time.Duration) (string, error) {
resp := &GenerateExpirationResponse{}
err := dr.client.Call("Plugin.GenerateExpiration", duration, resp)
return resp.Expiration, err
}
// ---- RPC server domain ----
// databasePluginRPCServer impliments DatabaseType and is run inside a plugin
type databasePluginRPCServer struct {
impl DatabaseType
}
func (ds *databasePluginRPCServer) Type(_ struct{}, resp *string) error {
*resp = ds.impl.Type()
return nil
}
func (ds *databasePluginRPCServer) CreateUser(args *CreateUserRequest, _ *struct{}) error {
err := ds.impl.CreateUser(args.Statements, args.Username, args.Password, args.Expiration)
return err
}
func (ds *databasePluginRPCServer) RenewUser(args *RenewUserRequest, _ *struct{}) error {
err := ds.impl.RenewUser(args.Statements, args.Username, args.Expiration)
return err
}
func (ds *databasePluginRPCServer) RevokeUser(args *RevokeUserRequest, _ *struct{}) error {
err := ds.impl.RevokeUser(args.Statements, args.Username)
return err
}
func (ds *databasePluginRPCServer) Initialize(args map[string]interface{}, _ *struct{}) error {
err := ds.impl.Initialize(args)
return err
}
func (ds *databasePluginRPCServer) Close(_ struct{}, _ *struct{}) error {
ds.impl.Close()
return nil
}
func (ds *databasePluginRPCServer) GenerateUsername(args string, resp *GenerateUsernameResponse) error {
var err error
resp.Username, err = ds.impl.GenerateUsername(args)
return err
}
func (ds *databasePluginRPCServer) GeneratePassword(_ struct{}, resp *GeneratePasswordResponse) error {
var err error
resp.Password, err = ds.impl.GeneratePassword()
return err
}
func (ds *databasePluginRPCServer) GenerateExpiration(args time.Duration, resp *GenerateExpirationResponse) error {
var err error
resp.Expiration, err = ds.impl.GenerateExpiration(args)
return err
}
// ---- Request Args Domain ----
type CreateUserRequest struct {
Statements Statements
Username string
Password string
Expiration string
}
type RenewUserRequest struct {
Statements Statements
Username string
Expiration string
}
type RevokeUserRequest struct {
Statements Statements
Username string
}
// ---- Response Args Domain ----
type GenerateUsernameResponse struct {
Username string
}
type GenerateExpirationResponse struct {
Expiration string
}
type GeneratePasswordResponse struct {
Password string
}

View File

@ -12,6 +12,11 @@ type Looker interface {
LookupPlugin(string) (*PluginRunner, error)
}
type LookWrapper interface {
Looker
Wrapper
}
type PluginRunner struct {
Name string `json:"name"`
Command string `json:"command"`