Merge pull request #2200 from hashicorp/database-refactor
Combined Database Backend with Plugins
This commit is contained in:
commit
258ac427ad
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/salt"
|
"github.com/hashicorp/vault/helper/salt"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
"github.com/mitchellh/reflectwalk"
|
"github.com/mitchellh/reflectwalk"
|
||||||
|
@ -84,7 +85,7 @@ func Hash(salter *salt.Salt, raw interface{}) error {
|
||||||
|
|
||||||
s.Data = data.(map[string]interface{})
|
s.Data = data.(map[string]interface{})
|
||||||
|
|
||||||
case *logical.ResponseWrapInfo:
|
case *wrapping.ResponseWrapInfo:
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/certutil"
|
"github.com/hashicorp/vault/helper/certutil"
|
||||||
"github.com/hashicorp/vault/helper/salt"
|
"github.com/hashicorp/vault/helper/salt"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
)
|
)
|
||||||
|
@ -69,7 +70,7 @@ func TestCopy_response(t *testing.T) {
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
WrapInfo: &logical.ResponseWrapInfo{
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
||||||
TTL: 60,
|
TTL: 60,
|
||||||
Token: "foo",
|
Token: "foo",
|
||||||
CreationTime: time.Now(),
|
CreationTime: time.Now(),
|
||||||
|
@ -140,7 +141,7 @@ func TestHash(t *testing.T) {
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
WrapInfo: &logical.ResponseWrapInfo{
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
||||||
TTL: 60,
|
TTL: 60,
|
||||||
Token: "bar",
|
Token: "bar",
|
||||||
CreationTime: now,
|
CreationTime: now,
|
||||||
|
@ -151,7 +152,7 @@ func TestHash(t *testing.T) {
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
||||||
},
|
},
|
||||||
WrapInfo: &logical.ResponseWrapInfo{
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
||||||
TTL: 60,
|
TTL: 60,
|
||||||
Token: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
Token: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
||||||
CreationTime: now,
|
CreationTime: now,
|
||||||
|
|
177
builtin/logical/database/backend.go
Normal file
177
builtin/logical/database/backend.go
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/rpc"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
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/config/"
|
||||||
|
|
||||||
|
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
|
||||||
|
return Backend(conf).Setup(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Backend(conf *logical.BackendConfig) *databaseBackend {
|
||||||
|
var b databaseBackend
|
||||||
|
b.Backend = &framework.Backend{
|
||||||
|
Help: strings.TrimSpace(backendHelp),
|
||||||
|
|
||||||
|
Paths: []*framework.Path{
|
||||||
|
pathConfigurePluginConnection(&b),
|
||||||
|
pathListRoles(&b),
|
||||||
|
pathRoles(&b),
|
||||||
|
pathCredsCreate(&b),
|
||||||
|
pathResetConnection(&b),
|
||||||
|
},
|
||||||
|
|
||||||
|
Secrets: []*framework.Secret{
|
||||||
|
secretCreds(&b),
|
||||||
|
},
|
||||||
|
|
||||||
|
Clean: b.closeAllDBs,
|
||||||
|
|
||||||
|
Invalidate: b.invalidate,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger = conf.Logger
|
||||||
|
b.connections = make(map[string]dbplugin.Database)
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
type databaseBackend struct {
|
||||||
|
connections map[string]dbplugin.Database
|
||||||
|
logger log.Logger
|
||||||
|
|
||||||
|
*framework.Backend
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeAllDBs closes all connections from all database types
|
||||||
|
func (b *databaseBackend) closeAllDBs() {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
for _, db := range b.connections {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.connections = make(map[string]dbplugin.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is used to retrieve a database object either from the cached
|
||||||
|
// connection map. The caller of this function needs to hold the backend's read
|
||||||
|
// lock.
|
||||||
|
func (b *databaseBackend) getDBObj(name string) (dbplugin.Database, bool) {
|
||||||
|
db, ok := b.connections[name]
|
||||||
|
return db, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function creates a new db object from the stored configuration and
|
||||||
|
// caches it in the connections map. The caller of this function needs to hold
|
||||||
|
// the backend's write lock
|
||||||
|
func (b *databaseBackend) createDBObj(s logical.Storage, name string) (dbplugin.Database, error) {
|
||||||
|
db, ok := b.connections[name]
|
||||||
|
if ok {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := b.DatabaseConfig(s, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err = dbplugin.PluginFactory(config.PluginName, b.System(), b.logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Initialize(config.ConnectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.connections[name] = db
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) DatabaseConfig(s logical.Storage, name string) (*DatabaseConfig, error) {
|
||||||
|
entry, err := s.Get(fmt.Sprintf("config/%s", name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read connection configuration: %s", err)
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, fmt.Errorf("failed to find entry for connection with name: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config DatabaseConfig
|
||||||
|
if err := entry.DecodeJSON(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) Role(s logical.Storage, roleName string) (*roleEntry, error) {
|
||||||
|
entry, err := s.Get("role/" + roleName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result roleEntry
|
||||||
|
if err := entry.DecodeJSON(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) invalidate(key string) {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(key, databaseConfigPath):
|
||||||
|
name := strings.TrimPrefix(key, databaseConfigPath)
|
||||||
|
b.clearConnection(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearConnection closes the database connection and
|
||||||
|
// removes it from the b.connections map.
|
||||||
|
func (b *databaseBackend) clearConnection(name string) {
|
||||||
|
db, ok := b.connections[name]
|
||||||
|
if ok {
|
||||||
|
db.Close()
|
||||||
|
delete(b.connections, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) closeIfShutdown(name string, err error) {
|
||||||
|
// Plugin has shutdown, close it so next call can reconnect.
|
||||||
|
if err == rpc.ErrShutdown {
|
||||||
|
b.Lock()
|
||||||
|
b.clearConnection(name)
|
||||||
|
b.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendHelp = `
|
||||||
|
The database backend supports using many different databases
|
||||||
|
as secret backends, including but not limited to:
|
||||||
|
cassandra, mssql, mysql, postgres
|
||||||
|
|
||||||
|
After mounting this backend, configure it using the endpoints within
|
||||||
|
the "database/config/" path.
|
||||||
|
`
|
766
builtin/logical/database/backend_test.go
Normal file
766
builtin/logical/database/backend_test.go
Normal file
|
@ -0,0 +1,766 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
stdhttp "net/http"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/http"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/postgresql"
|
||||||
|
"github.com/hashicorp/vault/vault"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testImagePull sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func preparePostgresTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cleanup func(), retURL string) {
|
||||||
|
if os.Getenv("PG_URL") != "" {
|
||||||
|
return func() {}, os.Getenv("PG_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := dockertest.NewPool("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=database"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not start local PostgreSQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = func() {
|
||||||
|
err := pool.Purge(resource)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cleanup local container: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retURL = fmt.Sprintf("postgres://postgres:secret@localhost:%s/database?sslmode=disable", resource.GetPort("5432/tcp"))
|
||||||
|
|
||||||
|
// exponential backoff-retry
|
||||||
|
if err = pool.Retry(func() error {
|
||||||
|
// This will cause a validation to run
|
||||||
|
resp, err := b.HandleRequest(&logical.Request{
|
||||||
|
Storage: s,
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/postgresql",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"connection_url": retURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
// It's likely not up and running yet, so return error and try again
|
||||||
|
return fmt.Errorf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected warning")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Could not connect to PostgreSQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCore(t *testing.T) ([]*vault.TestClusterCore, logical.SystemView) {
|
||||||
|
coreConfig := &vault.CoreConfig{
|
||||||
|
LogicalBackends: map[string]logical.Factory{
|
||||||
|
"database": Factory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler1 := stdhttp.NewServeMux()
|
||||||
|
handler2 := stdhttp.NewServeMux()
|
||||||
|
handler3 := stdhttp.NewServeMux()
|
||||||
|
|
||||||
|
// Chicken-and-egg: Handler needs a core. So we create handlers first, then
|
||||||
|
// add routes chained to a Handler-created handler.
|
||||||
|
cores := vault.TestCluster(t, []stdhttp.Handler{handler1, handler2, handler3}, coreConfig, false)
|
||||||
|
handler1.Handle("/", http.Handler(cores[0].Core))
|
||||||
|
handler2.Handle("/", http.Handler(cores[1].Core))
|
||||||
|
handler3.Handle("/", http.Handler(cores[2].Core))
|
||||||
|
|
||||||
|
core := cores[0]
|
||||||
|
|
||||||
|
sys := vault.TestDynamicSystemView(core.Core)
|
||||||
|
vault.TestAddTestPlugin(t, core.Core, "postgresql-database-plugin", "TestBackend_PluginMain")
|
||||||
|
|
||||||
|
return cores, sys
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_PluginMain(t *testing.T) {
|
||||||
|
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := []byte(vault.TestClusterCACert)
|
||||||
|
tmpfile, err := ioutil.TempFile("", "example")
|
||||||
|
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)
|
||||||
|
|
||||||
|
postgresql.Run(apiClientMeta.GetTLSConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_config_connection(t *testing.T) {
|
||||||
|
var resp *logical.Response
|
||||||
|
var err error
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Cleanup()
|
||||||
|
|
||||||
|
configData := map[string]interface{}{
|
||||||
|
"connection_url": "sample_connection_url",
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"verify_connection": false,
|
||||||
|
"allowed_roles": []string{"*"},
|
||||||
|
}
|
||||||
|
|
||||||
|
configReq := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: configData,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(configReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"connection_details": map[string]interface{}{
|
||||||
|
"connection_url": "sample_connection_url",
|
||||||
|
},
|
||||||
|
"allowed_roles": []string{"*"},
|
||||||
|
}
|
||||||
|
configReq.Operation = logical.ReadOperation
|
||||||
|
resp, err = b.HandleRequest(configReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(resp.Data["connection_details"].(map[string]interface{}), "name")
|
||||||
|
if !reflect.DeepEqual(expected, resp.Data) {
|
||||||
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_basic(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Cleanup()
|
||||||
|
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Configure a connection
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"allowed_roles": []string{"plugin-role-test"},
|
||||||
|
}
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a role
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"creation_statements": testRole,
|
||||||
|
"default_ttl": "5m",
|
||||||
|
"max_ttl": "10m",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (credsResp != nil && credsResp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCredsExist(t, credsResp, connURL) {
|
||||||
|
t.Fatalf("Creds should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke creds
|
||||||
|
resp, err = b.HandleRequest(&logical.Request{
|
||||||
|
Operation: logical.RevokeOperation,
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Secret: &logical.Secret{
|
||||||
|
InternalData: map[string]interface{}{
|
||||||
|
"secret_type": "creds",
|
||||||
|
"username": credsResp.Data["username"],
|
||||||
|
"role": "plugin-role-test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCredsExist(t, credsResp, connURL) {
|
||||||
|
t.Fatalf("Creds should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_connectionCrud(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Cleanup()
|
||||||
|
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Configure a connection
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection_url": "test",
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"verify_connection": false,
|
||||||
|
}
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a role
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"creation_statements": testRole,
|
||||||
|
"revocation_statements": defaultRevocationSQL,
|
||||||
|
"default_ttl": "5m",
|
||||||
|
"max_ttl": "10m",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the connection
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"allowed_roles": []string{"plugin-role-test"},
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read connection
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"connection_details": map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
},
|
||||||
|
"allowed_roles": []string{"plugin-role-test"},
|
||||||
|
}
|
||||||
|
req.Operation = logical.ReadOperation
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(resp.Data["connection_details"].(map[string]interface{}), "name")
|
||||||
|
if !reflect.DeepEqual(expected, resp.Data) {
|
||||||
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset Connection
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "reset/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (credsResp != nil && credsResp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCredsExist(t, credsResp, connURL) {
|
||||||
|
t.Fatalf("Creds should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Connection
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.DeleteOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read connection
|
||||||
|
req.Operation = logical.ReadOperation
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be empty
|
||||||
|
if resp != nil {
|
||||||
|
t.Fatal("Expected response to be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_roleCrud(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Cleanup()
|
||||||
|
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Configure a connection
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
}
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a role
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"creation_statements": testRole,
|
||||||
|
"revocation_statements": defaultRevocationSQL,
|
||||||
|
"default_ttl": "5m",
|
||||||
|
"max_ttl": "10m",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the role
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := dbplugin.Statements{
|
||||||
|
CreationStatements: testRole,
|
||||||
|
RevocationStatements: defaultRevocationSQL,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual dbplugin.Statements
|
||||||
|
if err := mapstructure.Decode(resp.Data, &actual); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expected, actual) {
|
||||||
|
t.Fatalf("Statements did not match, exepected %#v, got %#v", expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the role
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.DeleteOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the role
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be empty
|
||||||
|
if resp != nil {
|
||||||
|
t.Fatal("Expected response to be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestBackend_allowedRoles(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Cleanup()
|
||||||
|
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Configure a connection
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
}
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a denied and an allowed role
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"creation_statements": testRole,
|
||||||
|
"default_ttl": "5m",
|
||||||
|
"max_ttl": "10m",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/denied",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"creation_statements": testRole,
|
||||||
|
"default_ttl": "5m",
|
||||||
|
"max_ttl": "10m",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/allowed",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds from denied role, should fail
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/denied",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err := b.HandleRequest(req)
|
||||||
|
if err != logical.ErrPermissionDenied {
|
||||||
|
t.Fatalf("expected error to be:%s got:%#v\n", logical.ErrPermissionDenied, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update connection with * allowed roles connection
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"allowed_roles": "*",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds, should work.
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/allowed",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (credsResp != nil && credsResp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCredsExist(t, credsResp, connURL) {
|
||||||
|
t.Fatalf("Creds should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// update connection with allowed roles
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"allowed_roles": "allow, allowed",
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds from denied role, should fail
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/denied",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err = b.HandleRequest(req)
|
||||||
|
if err != logical.ErrPermissionDenied {
|
||||||
|
t.Fatalf("expected error to be:%s got:%#v\n", logical.ErrPermissionDenied, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creds from allowed role, should work.
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "creds/allowed",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
credsResp, err = b.HandleRequest(req)
|
||||||
|
if err != nil || (credsResp != nil && credsResp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCredsExist(t, credsResp, connURL) {
|
||||||
|
t.Fatalf("Creds should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCredsExist(t *testing.T, resp *logical.Response, connURL string) bool {
|
||||||
|
var d struct {
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}
|
||||||
|
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Printf("[TRACE] Generated credentials: %v", d)
|
||||||
|
conn, err := pq.ParseURL(connURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn += " timezone=utc"
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
returnedRows := func() int {
|
||||||
|
stmt, err := db.Prepare("SELECT DISTINCT schemaname FROM pg_tables WHERE has_table_privilege($1, 'information_schema.role_column_grants', 'select');")
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(d.Username)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for rows.Next() {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnedRows() == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const testRole = `
|
||||||
|
CREATE ROLE "{{name}}" WITH
|
||||||
|
LOGIN
|
||||||
|
PASSWORD '{{password}}'
|
||||||
|
VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultRevocationSQL = `
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE USAGE ON SCHEMA public FROM {{name}};
|
||||||
|
|
||||||
|
DROP ROLE IF EXISTS {{name}};
|
||||||
|
`
|
132
builtin/logical/database/dbplugin/client.go
Normal file
132
builtin/logical/database/dbplugin/client.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
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.RunnerUtil, pluginRunner *pluginutil.PluginRunner) (Database, 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 database type now. This feels like a normal interface
|
||||||
|
// implementation but is in fact over an RPC connection.
|
||||||
|
databaseRPC := raw.(*databasePluginRPCClient)
|
||||||
|
|
||||||
|
// Wrap RPC implimentation in DatabasePluginClient
|
||||||
|
return &DatabasePluginClient{
|
||||||
|
client: client,
|
||||||
|
databasePluginRPCClient: databaseRPC,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RPC client domain ----
|
||||||
|
|
||||||
|
// databasePluginRPCClient implements Database and is used on the client to
|
||||||
|
// make RPC calls to a plugin.
|
||||||
|
type databasePluginRPCClient struct {
|
||||||
|
client *rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *databasePluginRPCClient) Type() (string, error) {
|
||||||
|
var dbType string
|
||||||
|
err := dr.client.Call("Plugin.Type", struct{}{}, &dbType)
|
||||||
|
|
||||||
|
return fmt.Sprintf("plugin-%s", dbType), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *databasePluginRPCClient) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
req := CreateUserRequest{
|
||||||
|
Statements: statements,
|
||||||
|
UsernamePrefix: usernamePrefix,
|
||||||
|
Expiration: expiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp CreateUserResponse
|
||||||
|
err = dr.client.Call("Plugin.CreateUser", req, &resp)
|
||||||
|
|
||||||
|
return resp.Username, resp.Password, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *databasePluginRPCClient) RenewUser(statements Statements, username string, expiration time.Time) 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{}, verifyConnection bool) error {
|
||||||
|
req := InitializeRequest{
|
||||||
|
Config: conf,
|
||||||
|
VerifyConnection: verifyConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dr.client.Call("Plugin.Initialize", req, &struct{}{})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *databasePluginRPCClient) Close() error {
|
||||||
|
err := dr.client.Call("Plugin.Close", struct{}{}, &struct{}{})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
162
builtin/logical/database/dbplugin/databasemiddleware.go
Normal file
162
builtin/logical/database/dbplugin/databasemiddleware.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package dbplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metrics "github.com/armon/go-metrics"
|
||||||
|
log "github.com/mgutz/logxi/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Tracing Middleware Domain ----
|
||||||
|
|
||||||
|
// databaseTracingMiddleware wraps a implementation of Database and executes
|
||||||
|
// trace logging on function call.
|
||||||
|
type databaseTracingMiddleware struct {
|
||||||
|
next Database
|
||||||
|
logger log.Logger
|
||||||
|
|
||||||
|
typeStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) Type() (string, error) {
|
||||||
|
return mw.next.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
defer func(then time.Time) {
|
||||||
|
mw.logger.Trace("database", "operation", "CreateUser", "status", "finished", "type", mw.typeStr, "err", err, "took", time.Since(then))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
mw.logger.Trace("database", "operation", "CreateUser", "status", "started", "type", mw.typeStr)
|
||||||
|
return mw.next.CreateUser(statements, usernamePrefix, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) RenewUser(statements Statements, username string, expiration time.Time) (err error) {
|
||||||
|
defer func(then time.Time) {
|
||||||
|
mw.logger.Trace("database", "operation", "RenewUser", "status", "finished", "type", mw.typeStr, "err", err, "took", time.Since(then))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
mw.logger.Trace("database", "operation", "RenewUser", "status", "started", mw.typeStr)
|
||||||
|
return mw.next.RenewUser(statements, username, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) RevokeUser(statements Statements, username string) (err error) {
|
||||||
|
defer func(then time.Time) {
|
||||||
|
mw.logger.Trace("database", "operation", "RevokeUser", "status", "finished", "type", mw.typeStr, "err", err, "took", time.Since(then))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
mw.logger.Trace("database", "operation", "RevokeUser", "status", "started", "type", mw.typeStr)
|
||||||
|
return mw.next.RevokeUser(statements, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) Initialize(conf map[string]interface{}, verifyConnection bool) (err error) {
|
||||||
|
defer func(then time.Time) {
|
||||||
|
mw.logger.Trace("database", "operation", "Initialize", "status", "finished", "type", mw.typeStr, "verify", verifyConnection, "err", err, "took", time.Since(then))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
mw.logger.Trace("database", "operation", "Initialize", "status", "started", "type", mw.typeStr)
|
||||||
|
return mw.next.Initialize(conf, verifyConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseTracingMiddleware) Close() (err error) {
|
||||||
|
defer func(then time.Time) {
|
||||||
|
mw.logger.Trace("database", "operation", "Close", "status", "finished", "type", mw.typeStr, "err", err, "took", time.Since(then))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
mw.logger.Trace("database", "operation", "Close", "status", "started", "type", mw.typeStr)
|
||||||
|
return mw.next.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Metrics Middleware Domain ----
|
||||||
|
|
||||||
|
// databaseMetricsMiddleware wraps an implementation of Databases and on
|
||||||
|
// function call logs metrics about this instance.
|
||||||
|
type databaseMetricsMiddleware struct {
|
||||||
|
next Database
|
||||||
|
|
||||||
|
typeStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) Type() (string, error) {
|
||||||
|
return mw.next.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
defer func(now time.Time) {
|
||||||
|
metrics.MeasureSince([]string{"database", "CreateUser"}, now)
|
||||||
|
metrics.MeasureSince([]string{"database", mw.typeStr, "CreateUser"}, now)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounter([]string{"database", "CreateUser", "error"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "CreateUser", "error"}, 1)
|
||||||
|
}
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
metrics.IncrCounter([]string{"database", "CreateUser"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "CreateUser"}, 1)
|
||||||
|
return mw.next.CreateUser(statements, usernamePrefix, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) RenewUser(statements Statements, username string, expiration time.Time) (err error) {
|
||||||
|
defer func(now time.Time) {
|
||||||
|
metrics.MeasureSince([]string{"database", "RenewUser"}, now)
|
||||||
|
metrics.MeasureSince([]string{"database", mw.typeStr, "RenewUser"}, now)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounter([]string{"database", "RenewUser", "error"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "RenewUser", "error"}, 1)
|
||||||
|
}
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
metrics.IncrCounter([]string{"database", "RenewUser"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "RenewUser"}, 1)
|
||||||
|
return mw.next.RenewUser(statements, username, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) RevokeUser(statements Statements, username string) (err error) {
|
||||||
|
defer func(now time.Time) {
|
||||||
|
metrics.MeasureSince([]string{"database", "RevokeUser"}, now)
|
||||||
|
metrics.MeasureSince([]string{"database", mw.typeStr, "RevokeUser"}, now)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounter([]string{"database", "RevokeUser", "error"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "RevokeUser", "error"}, 1)
|
||||||
|
}
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
metrics.IncrCounter([]string{"database", "RevokeUser"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "RevokeUser"}, 1)
|
||||||
|
return mw.next.RevokeUser(statements, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) Initialize(conf map[string]interface{}, verifyConnection bool) (err error) {
|
||||||
|
defer func(now time.Time) {
|
||||||
|
metrics.MeasureSince([]string{"database", "Initialize"}, now)
|
||||||
|
metrics.MeasureSince([]string{"database", mw.typeStr, "Initialize"}, now)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounter([]string{"database", "Initialize", "error"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "Initialize", "error"}, 1)
|
||||||
|
}
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
metrics.IncrCounter([]string{"database", "Initialize"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "Initialize"}, 1)
|
||||||
|
return mw.next.Initialize(conf, verifyConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *databaseMetricsMiddleware) Close() (err error) {
|
||||||
|
defer func(now time.Time) {
|
||||||
|
metrics.MeasureSince([]string{"database", "Close"}, now)
|
||||||
|
metrics.MeasureSince([]string{"database", mw.typeStr, "Close"}, now)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
metrics.IncrCounter([]string{"database", "Close", "error"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "Close", "error"}, 1)
|
||||||
|
}
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
metrics.IncrCounter([]string{"database", "Close"}, 1)
|
||||||
|
metrics.IncrCounter([]string{"database", mw.typeStr, "Close"}, 1)
|
||||||
|
return mw.next.Close()
|
||||||
|
}
|
140
builtin/logical/database/dbplugin/plugin.go
Normal file
140
builtin/logical/database/dbplugin/plugin.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package dbplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/rpc"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
log "github.com/mgutz/logxi/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database is the interface that all database objects must implement.
|
||||||
|
type Database interface {
|
||||||
|
Type() (string, error)
|
||||||
|
CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error)
|
||||||
|
RenewUser(statements Statements, username string, expiration time.Time) error
|
||||||
|
RevokeUser(statements Statements, username string) error
|
||||||
|
|
||||||
|
Initialize(config map[string]interface{}, verifyConnection bool) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statements set in role creation and passed into the database type's functions.
|
||||||
|
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.LookRunnerUtil, logger log.Logger) (Database, error) {
|
||||||
|
// Look for plugin in the plugin catalog
|
||||||
|
pluginRunner, err := sys.LookupPlugin(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var db Database
|
||||||
|
if pluginRunner.Builtin {
|
||||||
|
// Plugin is builtin so we can retrieve an instance of the interface
|
||||||
|
// from the pluginRunner. Then cast it to a Database.
|
||||||
|
dbRaw, err := pluginRunner.BuiltinFactory()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting plugin type: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
db, ok = dbRaw.(Database)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsuported database type: %s", pluginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// create a DatabasePluginClient instance
|
||||||
|
db, err = newPluginClient(sys, pluginRunner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStr, err := db.Type()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting plugin type: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with metrics middleware
|
||||||
|
db = &databaseMetricsMiddleware{
|
||||||
|
next: db,
|
||||||
|
typeStr: typeStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with tracing middleware
|
||||||
|
if logger.IsTrace() {
|
||||||
|
db = &databaseTracingMiddleware{
|
||||||
|
next: db,
|
||||||
|
typeStr: typeStr,
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabasePlugin implements go-plugin's Plugin interface. It has methods for
|
||||||
|
// retrieving a server and a client instance of the plugin.
|
||||||
|
type DatabasePlugin struct {
|
||||||
|
impl Database
|
||||||
|
}
|
||||||
|
|
||||||
|
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 InitializeRequest struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
VerifyConnection bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserRequest struct {
|
||||||
|
Statements Statements
|
||||||
|
UsernamePrefix string
|
||||||
|
Expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenewUserRequest struct {
|
||||||
|
Statements Statements
|
||||||
|
Username string
|
||||||
|
Expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevokeUserRequest struct {
|
||||||
|
Statements Statements
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RPC Response Args Domain ----
|
||||||
|
|
||||||
|
type CreateUserResponse struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
248
builtin/logical/database/dbplugin/plugin_test.go
Normal file
248
builtin/logical/database/dbplugin/plugin_test.go
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
package dbplugin_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
stdhttp "net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/http"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
"github.com/hashicorp/vault/vault"
|
||||||
|
log "github.com/mgutz/logxi/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockPlugin struct {
|
||||||
|
users map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPlugin) Type() (string, error) { return "mock", nil }
|
||||||
|
func (m *mockPlugin) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
err = errors.New("err")
|
||||||
|
if usernamePrefix == "" || expiration.IsZero() {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.users[usernamePrefix]; ok {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.users[usernamePrefix] = []string{password}
|
||||||
|
|
||||||
|
return usernamePrefix, "test", nil
|
||||||
|
}
|
||||||
|
func (m *mockPlugin) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||||
|
err := errors.New("err")
|
||||||
|
if username == "" || expiration.IsZero() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.users[username]; !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockPlugin) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||||
|
err := errors.New("err")
|
||||||
|
if username == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.users[username]; !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(m.users, username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockPlugin) Initialize(conf map[string]interface{}, _ bool) error {
|
||||||
|
err := errors.New("err")
|
||||||
|
if len(conf) != 1 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockPlugin) Close() error {
|
||||||
|
m.users = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCore(t *testing.T) ([]*vault.TestClusterCore, logical.SystemView) {
|
||||||
|
coreConfig := &vault.CoreConfig{}
|
||||||
|
|
||||||
|
handler1 := stdhttp.NewServeMux()
|
||||||
|
handler2 := stdhttp.NewServeMux()
|
||||||
|
handler3 := stdhttp.NewServeMux()
|
||||||
|
|
||||||
|
// Chicken-and-egg: Handler needs a core. So we create handlers first, then
|
||||||
|
// add routes chained to a Handler-created handler.
|
||||||
|
cores := vault.TestCluster(t, []stdhttp.Handler{handler1, handler2, handler3}, coreConfig, false)
|
||||||
|
handler1.Handle("/", http.Handler(cores[0].Core))
|
||||||
|
handler2.Handle("/", http.Handler(cores[1].Core))
|
||||||
|
handler3.Handle("/", http.Handler(cores[2].Core))
|
||||||
|
|
||||||
|
core := cores[0]
|
||||||
|
|
||||||
|
sys := vault.TestDynamicSystemView(core.Core)
|
||||||
|
vault.TestAddTestPlugin(t, core.Core, "test-plugin", "TestPlugin_Main")
|
||||||
|
|
||||||
|
return cores, sys
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not an actual test case, it's a helper function that will be executed
|
||||||
|
// by the go-plugin client via an exec call.
|
||||||
|
func TestPlugin_Main(t *testing.T) {
|
||||||
|
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin := &mockPlugin{
|
||||||
|
users: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"--tls-skip-verify=true"}
|
||||||
|
|
||||||
|
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||||
|
flags := apiClientMeta.FlagSet()
|
||||||
|
flags.Parse(args)
|
||||||
|
|
||||||
|
plugins.Serve(plugin, apiClientMeta.GetTLSConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlugin_Initialize(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, err := dbplugin.PluginFactory("test-plugin", sys, &log.NullLogger{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"test": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbRaw.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbRaw.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlugin_CreateUser(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := dbplugin.PluginFactory("test-plugin", sys, &log.NullLogger{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"test": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
us, pw, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if us != "test" || pw != "test" {
|
||||||
|
t.Fatal("expected username and password to be 'test'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// try and save the same user again to verify it saved the first time, this
|
||||||
|
// should return an error
|
||||||
|
_, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error, user wasn't created correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlugin_RenewUser(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := dbplugin.PluginFactory("test-plugin", sys, &log.NullLogger{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"test": 1,
|
||||||
|
}
|
||||||
|
err = db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
us, _, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.RenewUser(dbplugin.Statements{}, us, time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlugin_RevokeUser(t *testing.T) {
|
||||||
|
cores, sys := getCore(t)
|
||||||
|
for _, core := range cores {
|
||||||
|
defer core.CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := dbplugin.PluginFactory("test-plugin", sys, &log.NullLogger{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"test": 1,
|
||||||
|
}
|
||||||
|
err = db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
us, _, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default revoke statememts
|
||||||
|
err = db.RevokeUser(dbplugin.Statements{}, us)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try adding the same username back so we can verify it was removed
|
||||||
|
_, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
71
builtin/logical/database/dbplugin/server.go
Normal file
71
builtin/logical/database/dbplugin/server.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package dbplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve is called from within a plugin and wraps the provided
|
||||||
|
// Database implementation in a databasePluginRPCServer object and starts a
|
||||||
|
// RPC server.
|
||||||
|
func Serve(db Database, tlsProvider func() (*tls.Config, error)) {
|
||||||
|
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: tlsProvider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RPC server domain ----
|
||||||
|
|
||||||
|
// databasePluginRPCServer implements an RPC version of Database and is run
|
||||||
|
// inside a plugin. It wraps an underlying implementation of Database.
|
||||||
|
type databasePluginRPCServer struct {
|
||||||
|
impl Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *databasePluginRPCServer) Type(_ struct{}, resp *string) error {
|
||||||
|
var err error
|
||||||
|
*resp, err = ds.impl.Type()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *databasePluginRPCServer) CreateUser(args *CreateUserRequest, resp *CreateUserResponse) error {
|
||||||
|
var err error
|
||||||
|
resp.Username, resp.Password, err = ds.impl.CreateUser(args.Statements, args.UsernamePrefix, 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 *InitializeRequest, _ *struct{}) error {
|
||||||
|
err := ds.impl.Initialize(args.Config, args.VerifyConnection)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *databasePluginRPCServer) Close(_ struct{}, _ *struct{}) error {
|
||||||
|
ds.impl.Close()
|
||||||
|
return nil
|
||||||
|
}
|
270
builtin/logical/database/path_config_connection.go
Normal file
270
builtin/logical/database/path_config_connection.go
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/structs"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
respErrEmptyPluginName = "empty plugin name"
|
||||||
|
respErrEmptyName = "empty name attribute given"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseConfig is used by the Factory function to configure a Database
|
||||||
|
// 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"`
|
||||||
|
AllowedRoles []string `json:"allowed_roles" structs:"allowed_roles" mapstructure:"allowed_roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathResetConnection configures a path to reset a plugin.
|
||||||
|
func pathResetConnection(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: fmt.Sprintf("reset/%s", framework.GenericNameRegex("name")),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of this database connection",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.UpdateOperation: b.pathConnectionReset(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathResetConnectionHelpSyn,
|
||||||
|
HelpDescription: pathResetConnectionHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathConnectionReset resets a plugin by closing the existing instance and
|
||||||
|
// creating a new one.
|
||||||
|
func (b *databaseBackend) pathConnectionReset() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
if name == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the mutex lock
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
// Close plugin and delete the entry in the connections cache.
|
||||||
|
b.clearConnection(name)
|
||||||
|
|
||||||
|
// Execute plugin again, we don't need the object so throw away.
|
||||||
|
_, err := b.createDBObj(req.Storage, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathConfigurePluginConnection returns a configured framework.Path setup to
|
||||||
|
// operate on plugins.
|
||||||
|
func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of this database connection",
|
||||||
|
},
|
||||||
|
|
||||||
|
"plugin_name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `The name of a builtin or previously registered
|
||||||
|
plugin known to vault. This endpoint will create an instance of
|
||||||
|
that plugin type.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"verify_connection": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeBool,
|
||||||
|
Default: true,
|
||||||
|
Description: `If true, the connection details are verified by
|
||||||
|
actually connecting to the database. Defaults to true.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"allowed_roles": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: `Comma separated string or array of the role names
|
||||||
|
allowed to get creds from this database connection. If empty no
|
||||||
|
roles are allowed. If "*" all roles are allowed.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.UpdateOperation: b.connectionWriteHandler(),
|
||||||
|
logical.ReadOperation: b.connectionReadHandler(),
|
||||||
|
logical.DeleteOperation: b.connectionDeleteHandler(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathConfigConnectionHelpSyn,
|
||||||
|
HelpDescription: pathConfigConnectionHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionReadHandler reads out the connection configuration
|
||||||
|
func (b *databaseBackend) connectionReadHandler() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
if name == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := req.Storage.Get(fmt.Sprintf("config/%s", name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to read connection configuration")
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var config DatabaseConfig
|
||||||
|
if err := entry.DecodeJSON(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &logical.Response{
|
||||||
|
Data: structs.New(config).Map(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionDeleteHandler deletes the connection configuration
|
||||||
|
func (b *databaseBackend) connectionDeleteHandler() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
if name == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := req.Storage.Delete(fmt.Sprintf("config/%s", name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to delete connection configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
if _, ok := b.connections[name]; ok {
|
||||||
|
err = b.connections[name].Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(b.connections, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionWriteHandler returns a handler function for creating and updating
|
||||||
|
// both builtin and plugin database types.
|
||||||
|
func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
pluginName := data.Get("plugin_name").(string)
|
||||||
|
if pluginName == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyPluginName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
if name == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyConnection := data.Get("verify_connection").(bool)
|
||||||
|
|
||||||
|
allowedRoles := data.Get("allowed_roles").([]string)
|
||||||
|
|
||||||
|
// Remove these entries from the data before we store it keyed under
|
||||||
|
// ConnectionDetails.
|
||||||
|
delete(data.Raw, "name")
|
||||||
|
delete(data.Raw, "plugin_name")
|
||||||
|
delete(data.Raw, "allowed_roles")
|
||||||
|
delete(data.Raw, "verify_connection")
|
||||||
|
|
||||||
|
config := &DatabaseConfig{
|
||||||
|
ConnectionDetails: data.Raw,
|
||||||
|
PluginName: pluginName,
|
||||||
|
AllowedRoles: allowedRoles,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Initialize(config.ConnectionDetails, verifyConnection)
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("error creating database object: %s", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the mutex lock
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
// Close and remove the old connection
|
||||||
|
b.clearConnection(name)
|
||||||
|
|
||||||
|
// Save the new connection
|
||||||
|
b.connections[name] = db
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON(fmt.Sprintf("config/%s", name), config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &logical.Response{}
|
||||||
|
resp.AddWarning("Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.")
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathConfigConnectionHelpSyn = `
|
||||||
|
Configure connection details to a database plugin.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathConfigConnectionHelpDesc = `
|
||||||
|
This path configures the connection details used to connect to a particular
|
||||||
|
database. This path runs the provided plugin name and passes the configured
|
||||||
|
connection details to the plugin. See the documentation for the plugin specified
|
||||||
|
for a full list of accepted connection details.
|
||||||
|
|
||||||
|
In addition to the database specific connection details, this endpoint also
|
||||||
|
accepts:
|
||||||
|
|
||||||
|
* "plugin_name" (required) - The name of a builtin or previously registered
|
||||||
|
plugin known to vault. This endpoint will create an instance of that
|
||||||
|
plugin type.
|
||||||
|
|
||||||
|
* "verify_connection" (default: true) - A boolean value denoting if the plugin should verify
|
||||||
|
it is able to connect to the database using the provided connection
|
||||||
|
details.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathResetConnectionHelpSyn = `
|
||||||
|
Resets a database plugin.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathResetConnectionHelpDesc = `
|
||||||
|
This path resets the database connection by closing the existing database plugin
|
||||||
|
instance and running a new one.
|
||||||
|
`
|
106
builtin/logical/database/path_creds_create.go
Normal file
106
builtin/logical/database/path_creds_create.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathCredsCreate(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "creds/" + framework.GenericNameRegex("name"),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the role.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.pathCredsCreateRead(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathCredsCreateReadHelpSyn,
|
||||||
|
HelpDescription: pathCredsCreateReadHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
|
||||||
|
// Get the role
|
||||||
|
role, err := b.Role(req.Storage, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConfig, err := b.DatabaseConfig(req.Storage, role.DBName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If role name isn't in the database's allowed roles, send back a
|
||||||
|
// permission denied.
|
||||||
|
if !strutil.StrListContains(dbConfig.AllowedRoles, "*") && !strutil.StrListContains(dbConfig.AllowedRoles, name) {
|
||||||
|
return nil, logical.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the read lock
|
||||||
|
b.RLock()
|
||||||
|
var unlockFunc func() = b.RUnlock
|
||||||
|
|
||||||
|
// Get the Database object
|
||||||
|
db, ok := b.getDBObj(role.DBName)
|
||||||
|
if !ok {
|
||||||
|
// Upgrade lock
|
||||||
|
b.RUnlock()
|
||||||
|
b.Lock()
|
||||||
|
unlockFunc = b.Unlock
|
||||||
|
|
||||||
|
// Create a new DB object
|
||||||
|
db, err = b.createDBObj(req.Storage, role.DBName)
|
||||||
|
if err != nil {
|
||||||
|
unlockFunc()
|
||||||
|
return nil, fmt.Errorf("cound not retrieve db with name: %s, got error: %s", role.DBName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration := time.Now().Add(role.DefaultTTL)
|
||||||
|
|
||||||
|
// Create the user
|
||||||
|
username, password, err := db.CreateUser(role.Statements, req.DisplayName, expiration)
|
||||||
|
// Unlock
|
||||||
|
unlockFunc()
|
||||||
|
if err != nil {
|
||||||
|
b.closeIfShutdown(role.DBName, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := b.Secret(SecretCredsType).Response(map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}, map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"role": name,
|
||||||
|
})
|
||||||
|
resp.Secret.TTL = role.DefaultTTL
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathCredsCreateReadHelpSyn = `
|
||||||
|
Request database credentials for a certain role.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathCredsCreateReadHelpDesc = `
|
||||||
|
This path reads database credentials for a certain role. The
|
||||||
|
database credentials will be generated on demand and will be automatically
|
||||||
|
revoked when the lease is up.
|
||||||
|
`
|
233
builtin/logical/database/path_roles.go
Normal file
233
builtin/logical/database/path_roles.go
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathListRoles(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "roles/?$",
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ListOperation: b.pathRoleList(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathRoleHelpSyn,
|
||||||
|
HelpDescription: pathRoleHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathRoles(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the role.",
|
||||||
|
},
|
||||||
|
|
||||||
|
"db_name": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the database this role acts on.",
|
||||||
|
},
|
||||||
|
"creation_statements": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `Statements to be executed to create a user. Must be a semicolon-separated
|
||||||
|
string, a base64-encoded semicolon-separated string, a serialized JSON string
|
||||||
|
array, or a base64-encoded serialized JSON string array. The '{{name}}',
|
||||||
|
'{{password}}', and '{{expiration}}' values will be substituted.`,
|
||||||
|
},
|
||||||
|
"revocation_statements": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `Statements to be executed to revoke a user. Must be a semicolon-separated
|
||||||
|
string, a base64-encoded semicolon-separated string, a serialized JSON string
|
||||||
|
array, or a base64-encoded serialized JSON string array. The '{{name}}' value
|
||||||
|
will be substituted.`,
|
||||||
|
},
|
||||||
|
"renew_statements": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `Statements to be executed to renew a user. Must be a semicolon-separated
|
||||||
|
string, a base64-encoded semicolon-separated string, a serialized JSON string
|
||||||
|
array, or a base64-encoded serialized JSON string array. The '{{name}}' value
|
||||||
|
will be substituted.`,
|
||||||
|
},
|
||||||
|
"rollback_statements": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `Statements to be executed to revoke a user. Must be a semicolon-separated
|
||||||
|
string, a base64-encoded semicolon-separated string, a serialized JSON string
|
||||||
|
array, or a base64-encoded serialized JSON string array. The '{{name}}' value
|
||||||
|
will be substituted.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"default_ttl": {
|
||||||
|
Type: framework.TypeDurationSecond,
|
||||||
|
Description: "Default ttl for role.",
|
||||||
|
},
|
||||||
|
|
||||||
|
"max_ttl": {
|
||||||
|
Type: framework.TypeDurationSecond,
|
||||||
|
Description: "Maximum time a credential is valid for",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.pathRoleRead(),
|
||||||
|
logical.UpdateOperation: b.pathRoleCreate(),
|
||||||
|
logical.DeleteOperation: b.pathRoleDelete(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathRoleHelpSyn,
|
||||||
|
HelpDescription: pathRoleHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) pathRoleDelete() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
err := req.Storage.Delete("role/" + data.Get("name").(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) pathRoleRead() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
role, err := b.Role(req.Storage, data.Get("name").(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"db_name": role.DBName,
|
||||||
|
"creation_statements": role.Statements.CreationStatements,
|
||||||
|
"revocation_statements": role.Statements.RevocationStatements,
|
||||||
|
"rollback_statements": role.Statements.RollbackStatements,
|
||||||
|
"renew_statements": role.Statements.RenewStatements,
|
||||||
|
"default_ttl": role.DefaultTTL.Seconds(),
|
||||||
|
"max_ttl": role.MaxTTL.Seconds(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) pathRoleList() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
entries, err := req.Storage.List("role/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logical.ListResponse(entries), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) pathRoleCreate() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
if name == "" {
|
||||||
|
return logical.ErrorResponse("empty role name attribute given"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbName := data.Get("db_name").(string)
|
||||||
|
if dbName == "" {
|
||||||
|
return logical.ErrorResponse("empty database name attribute given"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get statements
|
||||||
|
creationStmts := data.Get("creation_statements").(string)
|
||||||
|
revocationStmts := data.Get("revocation_statements").(string)
|
||||||
|
rollbackStmts := data.Get("rollback_statements").(string)
|
||||||
|
renewStmts := data.Get("renew_statements").(string)
|
||||||
|
|
||||||
|
// Get TTLs
|
||||||
|
defaultTTLRaw := data.Get("default_ttl").(int)
|
||||||
|
maxTTLRaw := data.Get("max_ttl").(int)
|
||||||
|
defaultTTL := time.Duration(defaultTTLRaw) * time.Second
|
||||||
|
maxTTL := time.Duration(maxTTLRaw) * time.Second
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: creationStmts,
|
||||||
|
RevocationStatements: revocationStmts,
|
||||||
|
RollbackStatements: rollbackStmts,
|
||||||
|
RenewStatements: renewStmts,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
||||||
|
DBName: dbName,
|
||||||
|
Statements: statements,
|
||||||
|
DefaultTTL: defaultTTL,
|
||||||
|
MaxTTL: maxTTL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type roleEntry struct {
|
||||||
|
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 = `
|
||||||
|
Manage the roles that can be created with this backend.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathRoleHelpDesc = `
|
||||||
|
This path lets you manage the roles that can be created with this backend.
|
||||||
|
|
||||||
|
The "db_name" parameter is required and configures the name of the database
|
||||||
|
connection to use.
|
||||||
|
|
||||||
|
The "creation_statements" parameter customizes the string used to create the
|
||||||
|
credentials. This can be a sequence of SQL queries, or other statement formats
|
||||||
|
for a particular database type. Some substitution will be done to the statement
|
||||||
|
strings for certain keys. The names of the variables must be surrounded by "{{"
|
||||||
|
and "}}" to be replaced.
|
||||||
|
|
||||||
|
* "name" - The random username generated for the DB user.
|
||||||
|
|
||||||
|
* "password" - The random password generated for the DB user.
|
||||||
|
|
||||||
|
* "expiration" - The timestamp when this user will expire.
|
||||||
|
|
||||||
|
Example of a decent creation_statements for a postgresql database plugin:
|
||||||
|
|
||||||
|
CREATE ROLE "{{name}}" WITH
|
||||||
|
LOGIN
|
||||||
|
PASSWORD '{{password}}'
|
||||||
|
VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
||||||
|
|
||||||
|
The "revocation_statements" parameter customizes the statement string used to
|
||||||
|
revoke a user. Example of a decent revocation_statements for a postgresql
|
||||||
|
database plugin:
|
||||||
|
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE USAGE ON SCHEMA public FROM {{name}};
|
||||||
|
DROP ROLE IF EXISTS {{name}};
|
||||||
|
|
||||||
|
The "renew_statements" parameter customizes the statement string used to renew a
|
||||||
|
user.
|
||||||
|
The "rollback_statements' parameter customizes the statement string used to
|
||||||
|
rollback a change if needed.
|
||||||
|
`
|
139
builtin/logical/database/secret_creds.go
Normal file
139
builtin/logical/database/secret_creds.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SecretCredsType = "creds"
|
||||||
|
|
||||||
|
func secretCreds(b *databaseBackend) *framework.Secret {
|
||||||
|
return &framework.Secret{
|
||||||
|
Type: SecretCredsType,
|
||||||
|
Fields: map[string]*framework.FieldSchema{},
|
||||||
|
|
||||||
|
Renew: b.secretCredsRenew(),
|
||||||
|
Revoke: b.secretCredsRevoke(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) secretCredsRenew() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
// Get the username from the internal data
|
||||||
|
usernameRaw, ok := req.Secret.InternalData["username"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("secret is missing username internal data")
|
||||||
|
}
|
||||||
|
username, ok := usernameRaw.(string)
|
||||||
|
|
||||||
|
roleNameRaw, ok := req.Secret.InternalData["role"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("could not find role with name: %s", req.Secret.InternalData["role"])
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := b.Role(req.Storage, roleNameRaw.(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return nil, fmt.Errorf("error during renew: could not find role with name %s", req.Secret.InternalData["role"])
|
||||||
|
}
|
||||||
|
|
||||||
|
f := framework.LeaseExtend(role.DefaultTTL, role.MaxTTL, b.System())
|
||||||
|
resp, err := f(req, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the read lock
|
||||||
|
b.RLock()
|
||||||
|
var unlockFunc func() = b.RUnlock
|
||||||
|
|
||||||
|
// Get the Database object
|
||||||
|
db, ok := b.getDBObj(role.DBName)
|
||||||
|
if !ok {
|
||||||
|
// Upgrade lock
|
||||||
|
b.RUnlock()
|
||||||
|
b.Lock()
|
||||||
|
unlockFunc = b.Unlock
|
||||||
|
|
||||||
|
// Create a new DB object
|
||||||
|
db, err = b.createDBObj(req.Storage, role.DBName)
|
||||||
|
if err != nil {
|
||||||
|
unlockFunc()
|
||||||
|
return nil, fmt.Errorf("cound not retrieve db with name: %s, got error: %s", role.DBName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we increase the VALID UNTIL endpoint for this user.
|
||||||
|
if expireTime := resp.Secret.ExpirationTime(); !expireTime.IsZero() {
|
||||||
|
err := db.RenewUser(role.Statements, username, expireTime)
|
||||||
|
// Unlock
|
||||||
|
unlockFunc()
|
||||||
|
if err != nil {
|
||||||
|
b.closeIfShutdown(role.DBName, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) secretCredsRevoke() framework.OperationFunc {
|
||||||
|
return func(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
// Get the username from the internal data
|
||||||
|
usernameRaw, ok := req.Secret.InternalData["username"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("secret is missing username internal data")
|
||||||
|
}
|
||||||
|
username, ok := usernameRaw.(string)
|
||||||
|
|
||||||
|
var resp *logical.Response
|
||||||
|
|
||||||
|
roleNameRaw, ok := req.Secret.InternalData["role"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no role name was provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := b.Role(req.Storage, roleNameRaw.(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return nil, fmt.Errorf("error during revoke: could not find role with name %s", req.Secret.InternalData["role"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the read lock
|
||||||
|
b.RLock()
|
||||||
|
var unlockFunc func() = b.RUnlock
|
||||||
|
|
||||||
|
// Get our connection
|
||||||
|
db, ok := b.getDBObj(role.DBName)
|
||||||
|
if !ok {
|
||||||
|
// Upgrade lock
|
||||||
|
b.RUnlock()
|
||||||
|
b.Lock()
|
||||||
|
unlockFunc = b.Unlock
|
||||||
|
|
||||||
|
// Create a new DB object
|
||||||
|
db, err = b.createDBObj(req.Storage, role.DBName)
|
||||||
|
if err != nil {
|
||||||
|
unlockFunc()
|
||||||
|
return nil, fmt.Errorf("cound not retrieve db with name: %s, got error: %s", role.DBName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.RevokeUser(role.Statements, username)
|
||||||
|
// Unlock
|
||||||
|
unlockFunc()
|
||||||
|
if err != nil {
|
||||||
|
b.closeIfShutdown(role.DBName, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/hashicorp/vault/builtin/logical/aws"
|
"github.com/hashicorp/vault/builtin/logical/aws"
|
||||||
"github.com/hashicorp/vault/builtin/logical/cassandra"
|
"github.com/hashicorp/vault/builtin/logical/cassandra"
|
||||||
"github.com/hashicorp/vault/builtin/logical/consul"
|
"github.com/hashicorp/vault/builtin/logical/consul"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database"
|
||||||
"github.com/hashicorp/vault/builtin/logical/mongodb"
|
"github.com/hashicorp/vault/builtin/logical/mongodb"
|
||||||
"github.com/hashicorp/vault/builtin/logical/mssql"
|
"github.com/hashicorp/vault/builtin/logical/mssql"
|
||||||
"github.com/hashicorp/vault/builtin/logical/mysql"
|
"github.com/hashicorp/vault/builtin/logical/mysql"
|
||||||
|
@ -92,6 +93,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
|
||||||
"mysql": mysql.Factory,
|
"mysql": mysql.Factory,
|
||||||
"ssh": ssh.Factory,
|
"ssh": ssh.Factory,
|
||||||
"rabbitmq": rabbitmq.Factory,
|
"rabbitmq": rabbitmq.Factory,
|
||||||
|
"database": database.Factory,
|
||||||
"totp": totp.Factory,
|
"totp": totp.Factory,
|
||||||
},
|
},
|
||||||
ShutdownCh: command.MakeShutdownCh(),
|
ShutdownCh: command.MakeShutdownCh(),
|
||||||
|
|
|
@ -238,6 +238,7 @@ func (c *ServerCommand) Run(args []string) int {
|
||||||
DefaultLeaseTTL: config.DefaultLeaseTTL,
|
DefaultLeaseTTL: config.DefaultLeaseTTL,
|
||||||
ClusterName: config.ClusterName,
|
ClusterName: config.ClusterName,
|
||||||
CacheSize: config.CacheSize,
|
CacheSize: config.CacheSize,
|
||||||
|
PluginDirectory: config.PluginDirectory,
|
||||||
}
|
}
|
||||||
if dev {
|
if dev {
|
||||||
coreConfig.DevToken = devRootTokenID
|
coreConfig.DevToken = devRootTokenID
|
||||||
|
|
|
@ -43,6 +43,7 @@ type Config struct {
|
||||||
DefaultLeaseTTLRaw interface{} `hcl:"default_lease_ttl"`
|
DefaultLeaseTTLRaw interface{} `hcl:"default_lease_ttl"`
|
||||||
|
|
||||||
ClusterName string `hcl:"cluster_name"`
|
ClusterName string `hcl:"cluster_name"`
|
||||||
|
PluginDirectory string `hcl:"plugin_directory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DevConfig is a Config that is used for dev mode of Vault.
|
// DevConfig is a Config that is used for dev mode of Vault.
|
||||||
|
@ -272,6 +273,11 @@ func (c *Config) Merge(c2 *Config) *Config {
|
||||||
result.EnableUI = c2.EnableUI
|
result.EnableUI = c2.EnableUI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.PluginDirectory = c.PluginDirectory
|
||||||
|
if c2.PluginDirectory != "" {
|
||||||
|
result.PluginDirectory = c2.PluginDirectory
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +369,7 @@ func ParseConfig(d string, logger log.Logger) (*Config, error) {
|
||||||
"default_lease_ttl",
|
"default_lease_ttl",
|
||||||
"max_lease_ttl",
|
"max_lease_ttl",
|
||||||
"cluster_name",
|
"cluster_name",
|
||||||
|
"plugin_directory",
|
||||||
}
|
}
|
||||||
if err := checkHCLKeys(list, valid); err != nil {
|
if err := checkHCLKeys(list, valid); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
40
helper/builtinplugins/builtin.go
Normal file
40
helper/builtinplugins/builtin.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package builtinplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/vault/plugins/database/cassandra"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/mssql"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/mysql"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/postgresql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuiltinFactory func() (interface{}, error)
|
||||||
|
|
||||||
|
var plugins map[string]BuiltinFactory = 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.DisplayNameLen, mysql.UsernameLen),
|
||||||
|
"mysql-aurora-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen),
|
||||||
|
"mysql-rds-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen),
|
||||||
|
"mysql-legacy-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen),
|
||||||
|
|
||||||
|
"postgresql-database-plugin": postgresql.New,
|
||||||
|
"mssql-database-plugin": mssql.New,
|
||||||
|
"cassandra-database-plugin": cassandra.New,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(name string) (BuiltinFactory, bool) {
|
||||||
|
f, ok := plugins[name]
|
||||||
|
return f, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func Keys() []string {
|
||||||
|
keys := make([]string, len(plugins))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for k := range plugins {
|
||||||
|
keys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
23
helper/pluginutil/mlock.go
Normal file
23
helper/pluginutil/mlock.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package pluginutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/mlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// PluginUnwrapTokenEnv is the ENV name used to pass the configuration for
|
||||||
|
// enabling mlock
|
||||||
|
PluginMlockEnabled = "VAULT_PLUGIN_MLOCK_ENABLED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptionallyEnableMlock determines if mlock should be called, and if so enables
|
||||||
|
// mlock.
|
||||||
|
func OptionallyEnableMlock() error {
|
||||||
|
if os.Getenv(PluginMlockEnabled) == "true" {
|
||||||
|
return mlock.LockMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
130
helper/pluginutil/runner.go
Normal file
130
helper/pluginutil/runner.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package pluginutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Looker defines the plugin Lookup function that looks into the plugin catalog
|
||||||
|
// for availible plugins and returns a PluginRunner
|
||||||
|
type Looker interface {
|
||||||
|
LookupPlugin(string) (*PluginRunner, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper interface defines the functions needed by the runner to wrap the
|
||||||
|
// metadata needed to run a plugin process. This includes looking up Mlock
|
||||||
|
// configuration and wrapping data in a respose wrapped token.
|
||||||
|
type RunnerUtil interface {
|
||||||
|
ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error)
|
||||||
|
MlockEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookWrapper defines the functions for both Looker and Wrapper
|
||||||
|
type LookRunnerUtil interface {
|
||||||
|
Looker
|
||||||
|
RunnerUtil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run takes a wrapper instance, and the go-plugin paramaters and executes a
|
||||||
|
// plugin.
|
||||||
|
func (r *PluginRunner) Run(wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string) (*plugin.Client, error) {
|
||||||
|
// Get a CA TLS Certificate
|
||||||
|
certBytes, key, err := generateCert()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use CA to sign a client cert and return a configured TLS config
|
||||||
|
clientTLSConfig, err := createClientTLSConfig(certBytes, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use CA to sign a server cert and wrap the values in a response wrapped
|
||||||
|
// token.
|
||||||
|
wrapToken, err := wrapServerConfig(wrapper, certBytes, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(r.Command, r.Args...)
|
||||||
|
cmd.Env = append(cmd.Env, env...)
|
||||||
|
// Add the response wrap token to the ENV of the plugin
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, wrapToken))
|
||||||
|
// Add the mlock setting to the ENV of the plugin
|
||||||
|
if wrapper.MlockEnabled() {
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", PluginMlockEnabled, "true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
secureConfig := &plugin.SecureConfig{
|
||||||
|
Checksum: r.Sha256,
|
||||||
|
Hash: sha256.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
client := plugin.NewClient(&plugin.ClientConfig{
|
||||||
|
HandshakeConfig: hs,
|
||||||
|
Plugins: pluginMap,
|
||||||
|
Cmd: cmd,
|
||||||
|
TLSConfig: clientTLSConfig,
|
||||||
|
SecureConfig: secureConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIClientMeta struct {
|
||||||
|
// These are set by the command line flags.
|
||||||
|
flagCACert string
|
||||||
|
flagCAPath string
|
||||||
|
flagClientCert string
|
||||||
|
flagClientKey string
|
||||||
|
flagInsecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *APIClientMeta) FlagSet() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("tls settings", flag.ContinueOnError)
|
||||||
|
|
||||||
|
fs.StringVar(&f.flagCACert, "ca-cert", "", "")
|
||||||
|
fs.StringVar(&f.flagCAPath, "ca-path", "", "")
|
||||||
|
fs.StringVar(&f.flagClientCert, "client-cert", "", "")
|
||||||
|
fs.StringVar(&f.flagClientKey, "client-key", "", "")
|
||||||
|
fs.BoolVar(&f.flagInsecure, "tls-skip-verify", false, "")
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *APIClientMeta) GetTLSConfig() *api.TLSConfig {
|
||||||
|
// If we need custom TLS configuration, then set it
|
||||||
|
if f.flagCACert != "" || f.flagCAPath != "" || f.flagClientCert != "" || f.flagClientKey != "" || f.flagInsecure {
|
||||||
|
t := &api.TLSConfig{
|
||||||
|
CACert: f.flagCACert,
|
||||||
|
CAPath: f.flagCAPath,
|
||||||
|
ClientCert: f.flagClientCert,
|
||||||
|
ClientKey: f.flagClientKey,
|
||||||
|
TLSServerName: "",
|
||||||
|
Insecure: f.flagInsecure,
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
227
helper/pluginutil/tls.go
Normal file
227
helper/pluginutil/tls.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
package pluginutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SermoDigital/jose/jws"
|
||||||
|
"github.com/hashicorp/errwrap"
|
||||||
|
uuid "github.com/hashicorp/go-uuid"
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/helper/certutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// PluginUnwrapTokenEnv is the ENV name used to pass unwrap tokens to the
|
||||||
|
// plugin.
|
||||||
|
PluginUnwrapTokenEnv = "VAULT_UNWRAP_TOKEN"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateCert is used internally to create certificates for the plugin
|
||||||
|
// client and server.
|
||||||
|
func generateCert() ([]byte, *ecdsa.PrivateKey, error) {
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sn, err := certutil.GenerateSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: host,
|
||||||
|
},
|
||||||
|
DNSNames: []string{host},
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
|
x509.ExtKeyUsageClientAuth,
|
||||||
|
x509.ExtKeyUsageServerAuth,
|
||||||
|
},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
||||||
|
SerialNumber: sn,
|
||||||
|
NotBefore: time.Now().Add(-30 * time.Second),
|
||||||
|
NotAfter: time.Now().Add(262980 * time.Hour),
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errwrap.Wrapf("unable to generate client certificate: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certBytes, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createClientTLSConfig creates a signed certificate and returns a configured
|
||||||
|
// TLS config.
|
||||||
|
func createClientTLSConfig(certBytes []byte, key *ecdsa.PrivateKey) (*tls.Config, error) {
|
||||||
|
clientCert, err := x509.ParseCertificate(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing generated plugin certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := tls.Certificate{
|
||||||
|
Certificate: [][]byte{certBytes},
|
||||||
|
PrivateKey: key,
|
||||||
|
Leaf: clientCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCertPool := x509.NewCertPool()
|
||||||
|
clientCertPool.AddCert(clientCert)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: clientCertPool,
|
||||||
|
ServerName: clientCert.Subject.CommonName,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig.BuildNameToCertificate()
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapServerConfig is used to create a server certificate and private key, then
|
||||||
|
// wrap them in an unwrap token for later retrieval by the plugin.
|
||||||
|
func wrapServerConfig(sys RunnerUtil, certBytes []byte, key *ecdsa.PrivateKey) (string, error) {
|
||||||
|
rawKey, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapInfo, err := sys.ResponseWrapData(map[string]interface{}{
|
||||||
|
"ServerCert": certBytes,
|
||||||
|
"ServerKey": rawKey,
|
||||||
|
}, time.Second*10, true)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapInfo.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultPluginTLSProvider is run inside a plugin and retrives the response
|
||||||
|
// wrapped TLS certificate from vault. It returns a configured TLS Config.
|
||||||
|
func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, error) {
|
||||||
|
return func() (*tls.Config, error) {
|
||||||
|
unwrapToken := os.Getenv(PluginUnwrapTokenEnv)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
if wt == nil {
|
||||||
|
return nil, errors.New("nil decoded token")
|
||||||
|
}
|
||||||
|
|
||||||
|
addrRaw := wt.Claims().Get("addr")
|
||||||
|
if addrRaw == nil {
|
||||||
|
return nil, errors.New("decoded token does not contain primary cluster address")
|
||||||
|
}
|
||||||
|
vaultAddr, ok := addrRaw.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("decoded token's address not valid")
|
||||||
|
}
|
||||||
|
if vaultAddr == "" {
|
||||||
|
return nil, errors.New(`no address for the vault found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check the value
|
||||||
|
if _, err := url.Parse(vaultAddr); err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("error parsing the vault address: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap the token
|
||||||
|
clientConf := api.DefaultConfig()
|
||||||
|
clientConf.Address = vaultAddr
|
||||||
|
if apiTLSConfig != nil {
|
||||||
|
clientConf.ConfigureTLS(apiTLSConfig)
|
||||||
|
}
|
||||||
|
client, err := api.NewClient(clientConf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("error during api client creation: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := client.Logical().Unwrap(unwrapToken)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and parse the server's certificate
|
||||||
|
serverCertBytesRaw, ok := secret.Data["ServerCert"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("error unmarshalling certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCertBytes, err := base64.StdEncoding.DecodeString(serverCertBytesRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCert, err := x509.ParseCertificate(serverCertBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and parse the server's private key
|
||||||
|
serverKeyB64, ok := secret.Data["ServerKey"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("error unmarshalling certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverKeyRaw, err := base64.StdEncoding.DecodeString(serverKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverKey, err := x509.ParseECPrivateKey(serverKeyRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CA cert to the cert pool
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AddCert(serverCert)
|
||||||
|
|
||||||
|
// Build a certificate object out of the server's cert and private key.
|
||||||
|
cert := tls.Certificate{
|
||||||
|
Certificate: [][]byte{serverCertBytes},
|
||||||
|
PrivateKey: serverKey,
|
||||||
|
Leaf: serverCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup TLS config
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
// TLS 1.2 minimum
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}
|
||||||
|
tlsConfig.BuildNameToCertificate()
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,19 @@ func StrListSubset(super, sub []string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses a comma separated list of strings into a slice of strings.
|
||||||
|
// The return slice will be sorted and will not contain duplicate or
|
||||||
|
// empty items.
|
||||||
|
func ParseDedupAndSortStrings(input string, sep string) []string {
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
parsed := []string{}
|
||||||
|
if input == "" {
|
||||||
|
// Don't return nil
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return RemoveDuplicates(strings.Split(input, sep), false)
|
||||||
|
}
|
||||||
|
|
||||||
// Parses a comma separated list of strings into a slice of strings.
|
// Parses a comma separated list of strings into a slice of strings.
|
||||||
// The return slice will be sorted and will not contain duplicate or
|
// The return slice will be sorted and will not contain duplicate or
|
||||||
// empty items. The values will be converted to lower case.
|
// empty items. The values will be converted to lower case.
|
||||||
|
|
23
helper/wrapping/wrapinfo.go
Normal file
23
helper/wrapping/wrapinfo.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package wrapping
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ResponseWrapInfo struct {
|
||||||
|
// Setting to non-zero specifies that the response should be wrapped.
|
||||||
|
// Specifies the desired TTL of the wrapping token.
|
||||||
|
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"`
|
||||||
|
|
||||||
|
// The token containing the wrapped response
|
||||||
|
Token string `json:"token" structs:"token" mapstructure:"token"`
|
||||||
|
|
||||||
|
// The creation time. This can be used with the TTL to figure out an
|
||||||
|
// expected expiration.
|
||||||
|
CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"cration_time"`
|
||||||
|
|
||||||
|
// If the contained response is the output of a token creation call, the
|
||||||
|
// created token's accessor will be accessible here
|
||||||
|
WrappedAccessor string `json:"wrapped_accessor" structs:"wrapped_accessor" mapstructure:"wrapped_accessor"`
|
||||||
|
|
||||||
|
// The format to use. This doesn't get returned, it's only internal.
|
||||||
|
Format string `json:"format" structs:"format" mapstructure:"format"`
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,26 +28,6 @@ const (
|
||||||
HTTPStatusCode = "http_status_code"
|
HTTPStatusCode = "http_status_code"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResponseWrapInfo struct {
|
|
||||||
// Setting to non-zero specifies that the response should be wrapped.
|
|
||||||
// Specifies the desired TTL of the wrapping token.
|
|
||||||
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"`
|
|
||||||
|
|
||||||
// The token containing the wrapped response
|
|
||||||
Token string `json:"token" structs:"token" mapstructure:"token"`
|
|
||||||
|
|
||||||
// The creation time. This can be used with the TTL to figure out an
|
|
||||||
// expected expiration.
|
|
||||||
CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"cration_time"`
|
|
||||||
|
|
||||||
// If the contained response is the output of a token creation call, the
|
|
||||||
// created token's accessor will be accessible here
|
|
||||||
WrappedAccessor string `json:"wrapped_accessor" structs:"wrapped_accessor" mapstructure:"wrapped_accessor"`
|
|
||||||
|
|
||||||
// The format to use. This doesn't get returned, it's only internal.
|
|
||||||
Format string `json:"format" structs:"format" mapstructure:"format"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is a struct that stores the response of a request.
|
// Response is a struct that stores the response of a request.
|
||||||
// It is used to abstract the details of the higher level request protocol.
|
// It is used to abstract the details of the higher level request protocol.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
|
@ -78,7 +58,7 @@ type Response struct {
|
||||||
warnings []string `json:"warnings" structs:"warnings" mapstructure:"warnings"`
|
warnings []string `json:"warnings" structs:"warnings" mapstructure:"warnings"`
|
||||||
|
|
||||||
// Information for wrapping the response in a cubbyhole
|
// Information for wrapping the response in a cubbyhole
|
||||||
WrapInfo *ResponseWrapInfo `json:"wrap_info" structs:"wrap_info" mapstructure:"wrap_info"`
|
WrapInfo *wrapping.ResponseWrapInfo `json:"wrap_info" structs:"wrap_info" mapstructure:"wrap_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -123,7 +103,7 @@ func init() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error copying WrapInfo: %v", err)
|
return nil, fmt.Errorf("error copying WrapInfo: %v", err)
|
||||||
}
|
}
|
||||||
ret.WrapInfo = retWrapInfo.(*ResponseWrapInfo)
|
ret.WrapInfo = retWrapInfo.(*wrapping.ResponseWrapInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package logical
|
package logical
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/consts"
|
"github.com/hashicorp/vault/helper/consts"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SystemView exposes system configuration information in a safe way
|
// SystemView exposes system configuration information in a safe way
|
||||||
|
@ -37,6 +40,18 @@ type SystemView interface {
|
||||||
|
|
||||||
// ReplicationState indicates the state of cluster replication
|
// ReplicationState indicates the state of cluster replication
|
||||||
ReplicationState() consts.ReplicationState
|
ReplicationState() consts.ReplicationState
|
||||||
|
|
||||||
|
// ResponseWrapData wraps the given data in a cubbyhole and returns the
|
||||||
|
// token used to unwrap.
|
||||||
|
ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error)
|
||||||
|
|
||||||
|
// LookupPlugin looks into the plugin catalog for a plugin with the given
|
||||||
|
// name. Returns a PluginRunner or an error if a plugin can not be found.
|
||||||
|
LookupPlugin(string) (*pluginutil.PluginRunner, error)
|
||||||
|
|
||||||
|
// MlockEnabled returns the configuration setting for enabling mlock on
|
||||||
|
// plugins.
|
||||||
|
MlockEnabled() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type StaticSystemView struct {
|
type StaticSystemView struct {
|
||||||
|
@ -46,6 +61,7 @@ type StaticSystemView struct {
|
||||||
TaintedVal bool
|
TaintedVal bool
|
||||||
CachingDisabledVal bool
|
CachingDisabledVal bool
|
||||||
Primary bool
|
Primary bool
|
||||||
|
EnableMlock bool
|
||||||
ReplicationStateVal consts.ReplicationState
|
ReplicationStateVal consts.ReplicationState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,3 +88,15 @@ func (d StaticSystemView) CachingDisabled() bool {
|
||||||
func (d StaticSystemView) ReplicationState() consts.ReplicationState {
|
func (d StaticSystemView) ReplicationState() consts.ReplicationState {
|
||||||
return d.ReplicationStateVal
|
return d.ReplicationStateVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d StaticSystemView) ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) {
|
||||||
|
return nil, errors.New("ResponseWrapData is not implemented in StaticSystemView")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d StaticSystemView) LookupPlugin(name string) (*pluginutil.PluginRunner, error) {
|
||||||
|
return nil, errors.New("LookupPlugin is not implemented in StaticSystemView")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d StaticSystemView) MlockEnabled() bool {
|
||||||
|
return d.EnableMlock
|
||||||
|
}
|
||||||
|
|
21
plugins/database/cassandra/cassandra-database-plugin/main.go
Normal file
21
plugins/database/cassandra/cassandra-database-plugin/main.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/cassandra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||||
|
flags := apiClientMeta.FlagSet()
|
||||||
|
flags.Parse(os.Args)
|
||||||
|
|
||||||
|
err := cassandra.Run(apiClientMeta.GetTLSConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
169
plugins/database/cassandra/cassandra.go
Normal file
169
plugins/database/cassandra/cassandra.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
package cassandra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gocql/gocql"
|
||||||
|
multierror "github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/credsutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultUserCreationCQL = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;`
|
||||||
|
defaultUserDeletionCQL = `DROP USER '{{username}}';`
|
||||||
|
cassandraTypeName = "cassandra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cassandra is an implementation of Database interface
|
||||||
|
type Cassandra struct {
|
||||||
|
connutil.ConnectionProducer
|
||||||
|
credsutil.CredentialsProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Cassandra instance
|
||||||
|
func New() (interface{}, error) {
|
||||||
|
connProducer := &connutil.CassandraConnectionProducer{}
|
||||||
|
connProducer.Type = cassandraTypeName
|
||||||
|
|
||||||
|
credsProducer := &credsutil.CassandraCredentialsProducer{}
|
||||||
|
|
||||||
|
dbType := &Cassandra{
|
||||||
|
ConnectionProducer: connProducer,
|
||||||
|
CredentialsProducer: credsProducer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run instantiates a Cassandra object, and runs the RPC server for the plugin
|
||||||
|
func Run(apiTLSConfig *api.TLSConfig) error {
|
||||||
|
dbType, err := New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.Serve(dbType.(*Cassandra), apiTLSConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the TypeName for this backend
|
||||||
|
func (c *Cassandra) Type() (string, error) {
|
||||||
|
return cassandraTypeName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cassandra) getConnection() (*gocql.Session, error) {
|
||||||
|
session, err := c.Connection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.(*gocql.Session), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser generates the username/password on the underlying Cassandra secret backend as instructed by
|
||||||
|
// the CreationStatement provided.
|
||||||
|
func (c *Cassandra) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
// Grab the lock
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
// Get the connection
|
||||||
|
session, err := c.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
creationCQL := statements.CreationStatements
|
||||||
|
if creationCQL == "" {
|
||||||
|
creationCQL = defaultUserCreationCQL
|
||||||
|
}
|
||||||
|
rollbackCQL := statements.RollbackStatements
|
||||||
|
if rollbackCQL == "" {
|
||||||
|
rollbackCQL = defaultUserDeletionCQL
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err = c.GenerateUsername(usernamePrefix)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err = c.GeneratePassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(creationCQL, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.Query(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})).Exec()
|
||||||
|
if err != nil {
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(rollbackCQL, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Query(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"username": username,
|
||||||
|
})).Exec()
|
||||||
|
}
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewUser is not supported on Cassandra, so this is a no-op.
|
||||||
|
func (c *Cassandra) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||||
|
// NOOP
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeUser attempts to drop the specified user.
|
||||||
|
func (c *Cassandra) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||||
|
// Grab the lock
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
session, err := c.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
revocationCQL := statements.RevocationStatements
|
||||||
|
if revocationCQL == "" {
|
||||||
|
revocationCQL = defaultUserDeletionCQL
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *multierror.Error
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(revocationCQL, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := session.Query(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"username": username,
|
||||||
|
})).Exec()
|
||||||
|
|
||||||
|
result = multierror.Append(result, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
230
plugins/database/cassandra/cassandra_test.go
Normal file
230
plugins/database/cassandra/cassandra_test.go
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
package cassandra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gocql/gocql"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareCassandraTestContainer(t *testing.T) (cleanup func(), retURL string) {
|
||||||
|
if os.Getenv("CASSANDRA_HOST") != "" {
|
||||||
|
return func() {}, os.Getenv("CASSANDRA_HOST")
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := dockertest.NewPool("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
cassandraMountPath := fmt.Sprintf("%s/test-fixtures/:/etc/cassandra/", cwd)
|
||||||
|
|
||||||
|
ro := &dockertest.RunOptions{
|
||||||
|
Repository: "cassandra",
|
||||||
|
Tag: "latest",
|
||||||
|
Mounts: []string{cassandraMountPath},
|
||||||
|
}
|
||||||
|
resource, err := pool.RunWithOptions(ro)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not start local cassandra docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = func() {
|
||||||
|
err := pool.Purge(resource)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cleanup local container: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retURL = fmt.Sprintf("localhost:%s", resource.GetPort("9042/tcp"))
|
||||||
|
port, _ := strconv.Atoi(resource.GetPort("9042/tcp"))
|
||||||
|
|
||||||
|
// exponential backoff-retry
|
||||||
|
if err = pool.Retry(func() error {
|
||||||
|
clusterConfig := gocql.NewCluster(retURL)
|
||||||
|
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
|
||||||
|
Username: "cassandra",
|
||||||
|
Password: "cassandra",
|
||||||
|
}
|
||||||
|
clusterConfig.ProtoVersion = 4
|
||||||
|
clusterConfig.Port = port
|
||||||
|
|
||||||
|
session, err := clusterConfig.CreateSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating session: %s", err)
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Could not connect to cassandra docker container: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCassandra_Initialize(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareCassandraTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"hosts": connURL,
|
||||||
|
"username": "cassandra",
|
||||||
|
"password": "cassandra",
|
||||||
|
"protocol_version": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*Cassandra)
|
||||||
|
connProducer := db.ConnectionProducer.(*connutil.CassandraConnectionProducer)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !connProducer.Initialized {
|
||||||
|
t.Fatal("Database should be initalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCassandra_CreateUser(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareCassandraTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"hosts": connURL,
|
||||||
|
"username": "cassandra",
|
||||||
|
"password": "cassandra",
|
||||||
|
"protocol_version": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*Cassandra)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testCassandraRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMyCassandra_RenewUser(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareCassandraTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"hosts": connURL,
|
||||||
|
"username": "cassandra",
|
||||||
|
"password": "cassandra",
|
||||||
|
"protocol_version": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*Cassandra)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testCassandraRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.RenewUser(statements, username, time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCassandra_RevokeUser(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareCassandraTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"hosts": connURL,
|
||||||
|
"username": "cassandra",
|
||||||
|
"password": "cassandra",
|
||||||
|
"protocol_version": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*Cassandra)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testCassandraRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default revoke statememts
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCredsExist(t testing.TB, connURL, username, password string) error {
|
||||||
|
clusterConfig := gocql.NewCluster(connURL)
|
||||||
|
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
clusterConfig.ProtoVersion = 4
|
||||||
|
|
||||||
|
session, err := clusterConfig.CreateSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating session: %s", err)
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCassandraRole = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;
|
||||||
|
GRANT ALL PERMISSIONS ON ALL KEYSPACES TO {{username}};`
|
1146
plugins/database/cassandra/test-fixtures/cassandra.yaml
Normal file
1146
plugins/database/cassandra/test-fixtures/cassandra.yaml
Normal file
File diff suppressed because it is too large
Load diff
21
plugins/database/mssql/mssql-database-plugin/main.go
Normal file
21
plugins/database/mssql/mssql-database-plugin/main.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/mssql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||||
|
flags := apiClientMeta.FlagSet()
|
||||||
|
flags.Parse(os.Args)
|
||||||
|
|
||||||
|
err := mssql.Run(apiClientMeta.GetTLSConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
318
plugins/database/mssql/mssql.go
Normal file
318
plugins/database/mssql/mssql.go
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/credsutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const msSQLTypeName = "mssql"
|
||||||
|
|
||||||
|
// MSSQL is an implementation of Database interface
|
||||||
|
type MSSQL struct {
|
||||||
|
connutil.ConnectionProducer
|
||||||
|
credsutil.CredentialsProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (interface{}, error) {
|
||||||
|
connProducer := &connutil.SQLConnectionProducer{}
|
||||||
|
connProducer.Type = msSQLTypeName
|
||||||
|
|
||||||
|
credsProducer := &credsutil.SQLCredentialsProducer{
|
||||||
|
DisplayNameLen: 20,
|
||||||
|
UsernameLen: 128,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbType := &MSSQL{
|
||||||
|
ConnectionProducer: connProducer,
|
||||||
|
CredentialsProducer: credsProducer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run instantiates a MSSQL object, and runs the RPC server for the plugin
|
||||||
|
func Run(apiTLSConfig *api.TLSConfig) error {
|
||||||
|
dbType, err := New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.Serve(dbType.(*MSSQL), apiTLSConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the TypeName for this backend
|
||||||
|
func (m *MSSQL) Type() (string, error) {
|
||||||
|
return msSQLTypeName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MSSQL) getConnection() (*sql.DB, error) {
|
||||||
|
db, err := m.Connection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.(*sql.DB), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser generates the username/password on the underlying MSSQL secret backend as instructed by
|
||||||
|
// the CreationStatement provided.
|
||||||
|
func (m *MSSQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
// Grab the lock
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
// Get the connection
|
||||||
|
db, err := m.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if statements.CreationStatements == "" {
|
||||||
|
return "", "", dbutil.ErrEmptyCreationStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err = m.GenerateUsername(usernamePrefix)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err = m.GeneratePassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationStr, err := m.GenerateExpiration(expiration)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(statements.CreationStatements, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
"password": password,
|
||||||
|
"expiration": expirationStr,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewUser is not supported on MSSQL, so this is a no-op.
|
||||||
|
func (m *MSSQL) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||||
|
// NOOP
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeUser attempts to drop the specified user. It will first attempt to disable login,
|
||||||
|
// then kill pending connections from that user, and finally drop the user and login from the
|
||||||
|
// database instance.
|
||||||
|
func (m *MSSQL) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||||
|
if statements.RevocationStatements == "" {
|
||||||
|
return m.revokeUserDefault(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection
|
||||||
|
db, err := m.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(statements.RevocationStatements, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MSSQL) revokeUserDefault(username string) error {
|
||||||
|
// Get connection
|
||||||
|
db, err := m.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// First disable server login
|
||||||
|
disableStmt, err := db.Prepare(fmt.Sprintf("ALTER LOGIN [%s] DISABLE;", username))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer disableStmt.Close()
|
||||||
|
if _, err := disableStmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for sessions for the login so that we can kill any outstanding
|
||||||
|
// sessions. There cannot be any active sessions before we drop the logins
|
||||||
|
// This isn't done in a transaction because even if we fail along the way,
|
||||||
|
// we want to remove as much access as possible
|
||||||
|
sessionStmt, err := db.Prepare(fmt.Sprintf(
|
||||||
|
"SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = '%s';", username))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sessionStmt.Close()
|
||||||
|
|
||||||
|
sessionRows, err := sessionStmt.Query()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sessionRows.Close()
|
||||||
|
|
||||||
|
var revokeStmts []string
|
||||||
|
for sessionRows.Next() {
|
||||||
|
var sessionID int
|
||||||
|
err = sessionRows.Scan(&sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
revokeStmts = append(revokeStmts, fmt.Sprintf("KILL %d;", sessionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for database users using undocumented stored procedure for now since
|
||||||
|
// it is the easiest way to get this information;
|
||||||
|
// we need to drop the database users before we can drop the login and the role
|
||||||
|
// This isn't done in a transaction because even if we fail along the way,
|
||||||
|
// we want to remove as much access as possible
|
||||||
|
stmt, err := db.Prepare(fmt.Sprintf("EXEC master.dbo.sp_msloginmappings '%s';", username))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var loginName, dbName, qUsername string
|
||||||
|
var aliasName sql.NullString
|
||||||
|
err = rows.Scan(&loginName, &dbName, &qUsername, &aliasName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
revokeStmts = append(revokeStmts, fmt.Sprintf(dropUserSQL, dbName, username, username))
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not stop on error, as we want to remove as
|
||||||
|
// many permissions as possible right now
|
||||||
|
var lastStmtError error
|
||||||
|
for _, query := range revokeStmts {
|
||||||
|
stmt, err := db.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
_, err = stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't drop if not all database users are dropped
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return fmt.Errorf("cound not generate sql statements for all rows: %s", rows.Err())
|
||||||
|
}
|
||||||
|
if lastStmtError != nil {
|
||||||
|
return fmt.Errorf("could not perform all sql statements: %s", lastStmtError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop this login
|
||||||
|
stmt, err = db.Prepare(fmt.Sprintf(dropLoginSQL, username, username))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropUserSQL = `
|
||||||
|
USE [%s]
|
||||||
|
IF EXISTS
|
||||||
|
(SELECT name
|
||||||
|
FROM sys.database_principals
|
||||||
|
WHERE name = N'%s')
|
||||||
|
BEGIN
|
||||||
|
DROP USER [%s]
|
||||||
|
END
|
||||||
|
`
|
||||||
|
|
||||||
|
const dropLoginSQL = `
|
||||||
|
IF EXISTS
|
||||||
|
(SELECT name
|
||||||
|
FROM master.sys.server_principals
|
||||||
|
WHERE name = N'%s')
|
||||||
|
BEGIN
|
||||||
|
DROP LOGIN [%s]
|
||||||
|
END
|
||||||
|
`
|
167
plugins/database/mssql/mssql_test.go
Normal file
167
plugins/database/mssql/mssql_test.go
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testMSQLImagePull sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMSSQL_Initialize(t *testing.T) {
|
||||||
|
if os.Getenv("MSSQL_URL") == "" || os.Getenv("VAULT_ACC") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connURL := os.Getenv("MSSQL_URL")
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*MSSQL)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connProducer := db.ConnectionProducer.(*connutil.SQLConnectionProducer)
|
||||||
|
if !connProducer.Initialized {
|
||||||
|
t.Fatal("Database should be initalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMSSQL_CreateUser(t *testing.T) {
|
||||||
|
if os.Getenv("MSSQL_URL") == "" || os.Getenv("VAULT_ACC") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connURL := os.Getenv("MSSQL_URL")
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*MSSQL)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with no configured Creation Statememt
|
||||||
|
_, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error when no creation statement is provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testMSSQLRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMSSQL_RevokeUser(t *testing.T) {
|
||||||
|
if os.Getenv("MSSQL_URL") == "" || os.Getenv("VAULT_ACC") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connURL := os.Getenv("MSSQL_URL")
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*MSSQL)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testMSSQLRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default revoke statememts
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err = db.CreateUser(statements, "test", time.Now().Add(2*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom revoke statememt
|
||||||
|
statements.RevocationStatements = testMSSQLDrop
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCredsExist(t testing.TB, connURL, username, password string) error {
|
||||||
|
// Log in with the new creds
|
||||||
|
parts := strings.Split(connURL, "@")
|
||||||
|
connURL = fmt.Sprintf("sqlserver://%s:%s@%s", username, password, parts[1])
|
||||||
|
db, err := sql.Open("mssql", connURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
return db.Ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMSSQLRole = `
|
||||||
|
CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
|
||||||
|
CREATE USER [{{name}}] FOR LOGIN [{{name}}];
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [{{name}}];`
|
||||||
|
|
||||||
|
const testMSSQLDrop = `
|
||||||
|
DROP USER [{{name}}];
|
||||||
|
DROP LOGIN [{{name}}];
|
||||||
|
`
|
21
plugins/database/mysql/mysql-database-plugin/main.go
Normal file
21
plugins/database/mysql/mysql-database-plugin/main.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||||
|
flags := apiClientMeta.FlagSet()
|
||||||
|
flags.Parse(os.Args)
|
||||||
|
|
||||||
|
err := mysql.Run(apiClientMeta.GetTLSConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
201
plugins/database/mysql/mysql.go
Normal file
201
plugins/database/mysql/mysql.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/credsutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMysqlRevocationStmts = `
|
||||||
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'%';
|
||||||
|
DROP USER '{{name}}'@'%'
|
||||||
|
`
|
||||||
|
mySQLTypeName = "mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DisplayNameLen int = 10
|
||||||
|
LegacyDisplayNameLen int = 4
|
||||||
|
UsernameLen int = 32
|
||||||
|
LegacyUsernameLen int = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
type MySQL struct {
|
||||||
|
connutil.ConnectionProducer
|
||||||
|
credsutil.CredentialsProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
// New implements builtinplugins.BuiltinFactory
|
||||||
|
func New(displayLen, usernameLen int) func() (interface{}, error) {
|
||||||
|
return func() (interface{}, error) {
|
||||||
|
connProducer := &connutil.SQLConnectionProducer{}
|
||||||
|
connProducer.Type = mySQLTypeName
|
||||||
|
|
||||||
|
credsProducer := &credsutil.SQLCredentialsProducer{
|
||||||
|
DisplayNameLen: displayLen,
|
||||||
|
UsernameLen: usernameLen,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbType := &MySQL{
|
||||||
|
ConnectionProducer: connProducer,
|
||||||
|
CredentialsProducer: credsProducer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbType, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run instantiates a MySQL object, and runs the RPC server for the plugin
|
||||||
|
func Run(apiTLSConfig *api.TLSConfig) error {
|
||||||
|
f := New(DisplayNameLen, UsernameLen)
|
||||||
|
dbType, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.Serve(dbType.(*MySQL), apiTLSConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MySQL) Type() (string, error) {
|
||||||
|
return mySQLTypeName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MySQL) getConnection() (*sql.DB, error) {
|
||||||
|
db, err := m.Connection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.(*sql.DB), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MySQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
// Grab the lock
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
// Get the connection
|
||||||
|
db, err := m.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if statements.CreationStatements == "" {
|
||||||
|
return "", "", dbutil.ErrEmptyCreationStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err = m.GenerateUsername(usernamePrefix)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err = m.GeneratePassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationStr, err := m.GenerateExpiration(expiration)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(statements.CreationStatements, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
"password": password,
|
||||||
|
"expiration": expirationStr,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOOP
|
||||||
|
func (m *MySQL) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MySQL) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||||
|
// Grab the read lock
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
// Get the connection
|
||||||
|
db, err := m.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
revocationStmts := statements.RevocationStatements
|
||||||
|
// Use a default SQL statement for revocation if one cannot be fetched from the role
|
||||||
|
if revocationStmts == "" {
|
||||||
|
revocationStmts = defaultMysqlRevocationStmts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(revocationStmts, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not a prepared statement because not all commands are supported
|
||||||
|
// 1295: This command is not supported in the prepared statement protocol yet
|
||||||
|
// Reference https://mariadb.com/kb/en/mariadb/prepare-statement/
|
||||||
|
query = strings.Replace(query, "{{name}}", username, -1)
|
||||||
|
_, err = tx.Exec(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
206
plugins/database/mysql/mysql_test.go
Normal file
206
plugins/database/mysql/mysql_test.go
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testMySQLImagePull sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareMySQLTestContainer(t *testing.T) (cleanup func(), retURL string) {
|
||||||
|
if os.Getenv("MYSQL_URL") != "" {
|
||||||
|
return func() {}, os.Getenv("MYSQL_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := dockertest.NewPool("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, err := pool.Run("mysql", "latest", []string{"MYSQL_ROOT_PASSWORD=secret"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not start local MySQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = func() {
|
||||||
|
err := pool.Purge(resource)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cleanup local container: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retURL = fmt.Sprintf("root:secret@(localhost:%s)/mysql?parseTime=true", resource.GetPort("3306/tcp"))
|
||||||
|
|
||||||
|
// exponential backoff-retry
|
||||||
|
if err = pool.Retry(func() error {
|
||||||
|
var err error
|
||||||
|
var db *sql.DB
|
||||||
|
db, err = sql.Open("mysql", retURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Ping()
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Could not connect to MySQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQL_Initialize(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareMySQLTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
f := New(DisplayNameLen, UsernameLen)
|
||||||
|
dbRaw, _ := f()
|
||||||
|
db := dbRaw.(*MySQL)
|
||||||
|
connProducer := db.ConnectionProducer.(*connutil.SQLConnectionProducer)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !connProducer.Initialized {
|
||||||
|
t.Fatal("Database should be initalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQL_CreateUser(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareMySQLTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
f := New(DisplayNameLen, UsernameLen)
|
||||||
|
dbRaw, _ := f()
|
||||||
|
db := dbRaw.(*MySQL)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with no configured Creation Statememt
|
||||||
|
_, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error when no creation statement is provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testMySQLRoleWildCard,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQL_RevokeUser(t *testing.T) {
|
||||||
|
cleanup, connURL := prepareMySQLTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
f := New(DisplayNameLen, UsernameLen)
|
||||||
|
dbRaw, _ := f()
|
||||||
|
db := dbRaw.(*MySQL)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testMySQLRoleWildCard,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default revoke statememts
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
statements.CreationStatements = testMySQLRoleWildCard
|
||||||
|
username, password, err = db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom revoke statements
|
||||||
|
statements.RevocationStatements = testMySQLRevocationSQL
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCredsExist(t testing.TB, connURL, username, password string) error {
|
||||||
|
// Log in with the new creds
|
||||||
|
connURL = strings.Replace(connURL, "root:secret", fmt.Sprintf("%s:%s", username, password), 1)
|
||||||
|
db, err := sql.Open("mysql", connURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
return db.Ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMySQLRoleWildCard = `
|
||||||
|
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
|
||||||
|
GRANT SELECT ON *.* TO '{{name}}'@'%';
|
||||||
|
`
|
||||||
|
const testMySQLRevocationSQL = `
|
||||||
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'%';
|
||||||
|
DROP USER '{{name}}'@'%';
|
||||||
|
`
|
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/database/postgresql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||||
|
flags := apiClientMeta.FlagSet()
|
||||||
|
flags.Parse(os.Args)
|
||||||
|
|
||||||
|
err := postgresql.Run(apiClientMeta.GetTLSConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
343
plugins/database/postgresql/postgresql.go
Normal file
343
plugins/database/postgresql/postgresql.go
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/credsutil"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/dbutil"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
const postgreSQLTypeName string = "postgres"
|
||||||
|
|
||||||
|
// New implements builtinplugins.BuiltinFactory
|
||||||
|
func New() (interface{}, error) {
|
||||||
|
connProducer := &connutil.SQLConnectionProducer{}
|
||||||
|
connProducer.Type = postgreSQLTypeName
|
||||||
|
|
||||||
|
credsProducer := &credsutil.SQLCredentialsProducer{
|
||||||
|
DisplayNameLen: 10,
|
||||||
|
UsernameLen: 63,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbType := &PostgreSQL{
|
||||||
|
ConnectionProducer: connProducer,
|
||||||
|
CredentialsProducer: credsProducer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run instantiates a PostgreSQL object, and runs the RPC server for the plugin
|
||||||
|
func Run(apiTLSConfig *api.TLSConfig) error {
|
||||||
|
dbType, err := New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.Serve(dbType.(*PostgreSQL), apiTLSConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgreSQL struct {
|
||||||
|
connutil.ConnectionProducer
|
||||||
|
credsutil.CredentialsProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) Type() (string, error) {
|
||||||
|
return postgreSQLTypeName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) getConnection() (*sql.DB, error) {
|
||||||
|
db, err := p.Connection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.(*sql.DB), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||||
|
if statements.CreationStatements == "" {
|
||||||
|
return "", "", dbutil.ErrEmptyCreationStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the lock
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
username, err = p.GenerateUsername(usernamePrefix)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err = p.GeneratePassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationStr, err := p.GenerateExpiration(expiration)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the connection
|
||||||
|
db, err := p.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
// Return the secret
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(statements.CreationStatements, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
"password": password,
|
||||||
|
"expiration": expirationStr,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||||
|
// Grab the lock
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
db, err := p.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationStr, err := p.GenerateExpiration(expiration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
"ALTER ROLE %s VALID UNTIL '%s';",
|
||||||
|
pq.QuoteIdentifier(username),
|
||||||
|
expirationStr)
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||||
|
// Grab the lock
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if statements.RevocationStatements == "" {
|
||||||
|
return p.defaultRevokeUser(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.customRevokeUser(username, statements.RevocationStatements)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) customRevokeUser(username, revocationStmts string) error {
|
||||||
|
db, err := p.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(revocationStmts, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PostgreSQL) defaultRevokeUser(username string) error {
|
||||||
|
db, err := p.getConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the role exists
|
||||||
|
var exists bool
|
||||||
|
err = db.QueryRow("SELECT exists (SELECT rolname FROM pg_roles WHERE rolname=$1);", username).Scan(&exists)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for permissions; we need to revoke permissions before we can drop
|
||||||
|
// the role
|
||||||
|
// This isn't done in a transaction because even if we fail along the way,
|
||||||
|
// we want to remove as much access as possible
|
||||||
|
stmt, err := db.Prepare("SELECT DISTINCT table_schema FROM information_schema.role_column_grants WHERE grantee=$1;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
const initialNumRevocations = 16
|
||||||
|
revocationStmts := make([]string, 0, initialNumRevocations)
|
||||||
|
for rows.Next() {
|
||||||
|
var schema string
|
||||||
|
err = rows.Scan(&schema)
|
||||||
|
if err != nil {
|
||||||
|
// keep going; remove as many permissions as possible right now
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
`REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %s FROM %s;`,
|
||||||
|
pq.QuoteIdentifier(schema),
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
`REVOKE USAGE ON SCHEMA %s FROM %s;`,
|
||||||
|
pq.QuoteIdentifier(schema),
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// for good measure, revoke all privileges and usage on schema public
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
`REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM %s;`,
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
"REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM %s;",
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
"REVOKE USAGE ON SCHEMA public FROM %s;",
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
|
||||||
|
// get the current database name so we can issue a REVOKE CONNECT for
|
||||||
|
// this username
|
||||||
|
var dbname sql.NullString
|
||||||
|
if err := db.QueryRow("SELECT current_database();").Scan(&dbname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbname.Valid {
|
||||||
|
revocationStmts = append(revocationStmts, fmt.Sprintf(
|
||||||
|
`REVOKE CONNECT ON DATABASE %s FROM %s;`,
|
||||||
|
pq.QuoteIdentifier(dbname.String),
|
||||||
|
pq.QuoteIdentifier(username)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// again, here, we do not stop on error, as we want to remove as
|
||||||
|
// many permissions as possible right now
|
||||||
|
var lastStmtError error
|
||||||
|
for _, query := range revocationStmts {
|
||||||
|
stmt, err := db.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
_, err = stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't drop if not all privileges are revoked
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return fmt.Errorf("could not generate revocation statements for all rows: %s", rows.Err())
|
||||||
|
}
|
||||||
|
if lastStmtError != nil {
|
||||||
|
return fmt.Errorf("could not perform all revocation statements: %s", lastStmtError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop this user
|
||||||
|
stmt, err = db.Prepare(fmt.Sprintf(
|
||||||
|
`DROP ROLE IF EXISTS %s;`, pq.QuoteIdentifier(username)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
313
plugins/database/postgresql/postgresql_test.go
Normal file
313
plugins/database/postgresql/postgresql_test.go
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||||
|
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testPostgresImagePull sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func preparePostgresTestContainer(t *testing.T) (cleanup func(), retURL string) {
|
||||||
|
if os.Getenv("PG_URL") != "" {
|
||||||
|
return func() {}, os.Getenv("PG_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := dockertest.NewPool("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=database"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not start local PostgreSQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = func() {
|
||||||
|
err := pool.Purge(resource)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to cleanup local container: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retURL = fmt.Sprintf("postgres://postgres:secret@localhost:%s/database?sslmode=disable", resource.GetPort("5432/tcp"))
|
||||||
|
|
||||||
|
// exponential backoff-retry
|
||||||
|
if err = pool.Retry(func() error {
|
||||||
|
var err error
|
||||||
|
var db *sql.DB
|
||||||
|
db, err = sql.Open("postgres", retURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Ping()
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Could not connect to PostgreSQL docker container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQL_Initialize(t *testing.T) {
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*PostgreSQL)
|
||||||
|
|
||||||
|
connProducer := db.ConnectionProducer.(*connutil.SQLConnectionProducer)
|
||||||
|
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !connProducer.Initialized {
|
||||||
|
t.Fatal("Database should be initalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQL_CreateUser(t *testing.T) {
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*PostgreSQL)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with no configured Creation Statememt
|
||||||
|
_, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error when no creation statement is provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testPostgresRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements.CreationStatements = testPostgresReadOnlyRole
|
||||||
|
username, password, err = db.CreateUser(statements, "test", time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQL_RenewUser(t *testing.T) {
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*PostgreSQL)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testPostgresRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.RenewUser(statements, username, time.Now().Add(time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep longer than the inital expiration time
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQL_RevokeUser(t *testing.T) {
|
||||||
|
cleanup, connURL := preparePostgresTestContainer(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
connectionDetails := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRaw, _ := New()
|
||||||
|
db := dbRaw.(*PostgreSQL)
|
||||||
|
err := db.Initialize(connectionDetails, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statements := dbplugin.Statements{
|
||||||
|
CreationStatements: testPostgresRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default revoke statememts
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err = db.CreateUser(statements, "test", time.Now().Add(2*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testCredsExist(t, connURL, username, password); err != nil {
|
||||||
|
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom revoke statements
|
||||||
|
statements.RevocationStatements = defaultPostgresRevocationSQL
|
||||||
|
err = db.RevokeUser(statements, username)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := testCredsExist(t, connURL, username, password); err == nil {
|
||||||
|
t.Fatal("Credentials were not revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCredsExist(t testing.TB, connURL, username, password string) error {
|
||||||
|
// Log in with the new creds
|
||||||
|
connURL = strings.Replace(connURL, "postgres:secret", fmt.Sprintf("%s:%s", username, password), 1)
|
||||||
|
db, err := sql.Open("postgres", connURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
return db.Ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
const testPostgresRole = `
|
||||||
|
CREATE ROLE "{{name}}" WITH
|
||||||
|
LOGIN
|
||||||
|
PASSWORD '{{password}}'
|
||||||
|
VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
||||||
|
`
|
||||||
|
|
||||||
|
const testPostgresReadOnlyRole = `
|
||||||
|
CREATE ROLE "{{name}}" WITH
|
||||||
|
LOGIN
|
||||||
|
PASSWORD '{{password}}'
|
||||||
|
VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
||||||
|
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "{{name}}";
|
||||||
|
`
|
||||||
|
|
||||||
|
const testPostgresBlockStatementRole = `
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
||||||
|
CREATE ROLE "foo-role";
|
||||||
|
CREATE SCHEMA IF NOT EXISTS foo AUTHORIZATION "foo-role";
|
||||||
|
ALTER ROLE "foo-role" SET search_path = foo;
|
||||||
|
GRANT TEMPORARY ON DATABASE "postgres" TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$
|
||||||
|
|
||||||
|
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT "foo-role" TO "{{name}}";
|
||||||
|
ALTER ROLE "{{name}}" SET search_path = foo;
|
||||||
|
GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";
|
||||||
|
`
|
||||||
|
|
||||||
|
var testPostgresBlockStatementRoleSlice = []string{
|
||||||
|
`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
||||||
|
CREATE ROLE "foo-role";
|
||||||
|
CREATE SCHEMA IF NOT EXISTS foo AUTHORIZATION "foo-role";
|
||||||
|
ALTER ROLE "foo-role" SET search_path = foo;
|
||||||
|
GRANT TEMPORARY ON DATABASE "postgres" TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA foo TO "foo-role";
|
||||||
|
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$
|
||||||
|
`,
|
||||||
|
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';`,
|
||||||
|
`GRANT "foo-role" TO "{{name}}";`,
|
||||||
|
`ALTER ROLE "{{name}}" SET search_path = foo;`,
|
||||||
|
`GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPostgresRevocationSQL = `
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{name}}";
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{name}}";
|
||||||
|
REVOKE USAGE ON SCHEMA public FROM "{{name}}";
|
||||||
|
|
||||||
|
DROP ROLE IF EXISTS "{{name}}";
|
||||||
|
`
|
226
plugins/helper/database/connutil/cassandra.go
Normal file
226
plugins/helper/database/connutil/cassandra.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package connutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
|
||||||
|
"github.com/gocql/gocql"
|
||||||
|
"github.com/hashicorp/vault/helper/certutil"
|
||||||
|
"github.com/hashicorp/vault/helper/parseutil"
|
||||||
|
"github.com/hashicorp/vault/helper/tlsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CassandraConnectionProducer implements ConnectionProducer and provides an
|
||||||
|
// interface for cassandra databases to make connections.
|
||||||
|
type CassandraConnectionProducer struct {
|
||||||
|
Hosts string `json:"hosts" structs:"hosts" mapstructure:"hosts"`
|
||||||
|
Username string `json:"username" structs:"username" mapstructure:"username"`
|
||||||
|
Password string `json:"password" structs:"password" mapstructure:"password"`
|
||||||
|
TLS bool `json:"tls" structs:"tls" mapstructure:"tls"`
|
||||||
|
InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls" mapstructure:"insecure_tls"`
|
||||||
|
ProtocolVersion int `json:"protocol_version" structs:"protocol_version" mapstructure:"protocol_version"`
|
||||||
|
ConnectTimeoutRaw interface{} `json:"connect_timeout" structs:"connect_timeout" mapstructure:"connect_timeout"`
|
||||||
|
TLSMinVersion string `json:"tls_min_version" structs:"tls_min_version" mapstructure:"tls_min_version"`
|
||||||
|
Consistency string `json:"consistency" structs:"consistency" mapstructure:"consistency"`
|
||||||
|
PemBundle string `json:"pem_bundle" structs:"pem_bundle" mapstructure:"pem_bundle"`
|
||||||
|
PemJSON string `json:"pem_json" structs:"pem_json" mapstructure:"pem_json"`
|
||||||
|
|
||||||
|
connectTimeout time.Duration
|
||||||
|
certificate string
|
||||||
|
privateKey string
|
||||||
|
issuingCA string
|
||||||
|
|
||||||
|
Initialized bool
|
||||||
|
Type string
|
||||||
|
session *gocql.Session
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CassandraConnectionProducer) Initialize(conf map[string]interface{}, verifyConnection bool) error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
err := mapstructure.Decode(conf, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Initialized = true
|
||||||
|
|
||||||
|
if c.ConnectTimeoutRaw == nil {
|
||||||
|
c.ConnectTimeoutRaw = "0s"
|
||||||
|
}
|
||||||
|
c.connectTimeout, err = parseutil.ParseDurationSecond(c.ConnectTimeoutRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid connect_timeout: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(c.Hosts) == 0:
|
||||||
|
return fmt.Errorf("hosts cannot be empty")
|
||||||
|
case len(c.Username) == 0:
|
||||||
|
return fmt.Errorf("username cannot be empty")
|
||||||
|
case len(c.Password) == 0:
|
||||||
|
return fmt.Errorf("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var certBundle *certutil.CertBundle
|
||||||
|
var parsedCertBundle *certutil.ParsedCertBundle
|
||||||
|
switch {
|
||||||
|
case len(c.PemJSON) != 0:
|
||||||
|
parsedCertBundle, err = certutil.ParsePKIJSON([]byte(c.PemJSON))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse given JSON; it must be in the format of the output of the PKI backend certificate issuing command: %s", err)
|
||||||
|
}
|
||||||
|
certBundle, err = parsedCertBundle.ToCertBundle()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error marshaling PEM information: %s", err)
|
||||||
|
}
|
||||||
|
c.certificate = certBundle.Certificate
|
||||||
|
c.privateKey = certBundle.PrivateKey
|
||||||
|
c.issuingCA = certBundle.IssuingCA
|
||||||
|
c.TLS = true
|
||||||
|
|
||||||
|
case len(c.PemBundle) != 0:
|
||||||
|
parsedCertBundle, err = certutil.ParsePEMBundle(c.PemBundle)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error parsing the given PEM information: %s", err)
|
||||||
|
}
|
||||||
|
certBundle, err = parsedCertBundle.ToCertBundle()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error marshaling PEM information: %s", err)
|
||||||
|
}
|
||||||
|
c.certificate = certBundle.Certificate
|
||||||
|
c.privateKey = certBundle.PrivateKey
|
||||||
|
c.issuingCA = certBundle.IssuingCA
|
||||||
|
c.TLS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if verifyConnection {
|
||||||
|
if _, err := c.Connection(); err != nil {
|
||||||
|
return fmt.Errorf("error Initalizing Connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CassandraConnectionProducer) Connection() (interface{}, error) {
|
||||||
|
if !c.Initialized {
|
||||||
|
return nil, errNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already have a DB, return it
|
||||||
|
if c.session != nil {
|
||||||
|
return c.session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := c.createSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the session in backend for reuse
|
||||||
|
c.session = session
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CassandraConnectionProducer) Close() error {
|
||||||
|
// Grab the write lock
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
if c.session != nil {
|
||||||
|
c.session.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.session = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CassandraConnectionProducer) createSession() (*gocql.Session, error) {
|
||||||
|
clusterConfig := gocql.NewCluster(strings.Split(c.Hosts, ",")...)
|
||||||
|
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
|
||||||
|
Username: c.Username,
|
||||||
|
Password: c.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterConfig.ProtoVersion = c.ProtocolVersion
|
||||||
|
if clusterConfig.ProtoVersion == 0 {
|
||||||
|
clusterConfig.ProtoVersion = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterConfig.Timeout = c.connectTimeout
|
||||||
|
if c.TLS {
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if len(c.certificate) > 0 || len(c.issuingCA) > 0 {
|
||||||
|
if len(c.certificate) > 0 && len(c.privateKey) == 0 {
|
||||||
|
return nil, fmt.Errorf("found certificate for TLS authentication but no private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
certBundle := &certutil.CertBundle{}
|
||||||
|
if len(c.certificate) > 0 {
|
||||||
|
certBundle.Certificate = c.certificate
|
||||||
|
certBundle.PrivateKey = c.privateKey
|
||||||
|
}
|
||||||
|
if len(c.issuingCA) > 0 {
|
||||||
|
certBundle.IssuingCA = c.issuingCA
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedCertBundle, err := certBundle.ToParsedCertBundle()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse certificate bundle: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig, err = parsedCertBundle.GetTLSConfig(certutil.TLSClient)
|
||||||
|
if err != nil || tlsConfig == nil {
|
||||||
|
return nil, fmt.Errorf("failed to get TLS configuration: tlsConfig:%#v err:%v", tlsConfig, err)
|
||||||
|
}
|
||||||
|
tlsConfig.InsecureSkipVerify = c.InsecureTLS
|
||||||
|
|
||||||
|
if c.TLSMinVersion != "" {
|
||||||
|
var ok bool
|
||||||
|
tlsConfig.MinVersion, ok = tlsutil.TLSLookup[c.TLSMinVersion]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid 'tls_min_version' in config")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MinVersion was not being set earlier. Reset it to
|
||||||
|
// zero to gracefully handle upgrades.
|
||||||
|
tlsConfig.MinVersion = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterConfig.SslOpts = &gocql.SslOptions{
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := clusterConfig.CreateSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating session: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set consistency
|
||||||
|
if c.Consistency != "" {
|
||||||
|
consistencyValue, err := gocql.ParseConsistencyWrapper(c.Consistency)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SetConsistency(consistencyValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the info
|
||||||
|
err = session.Query(`LIST USERS`).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error validating connection info: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
21
plugins/helper/database/connutil/connutil.go
Normal file
21
plugins/helper/database/connutil/connutil.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package connutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotInitialized = errors.New("connection has not been initalized")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionProducer can be used as an embeded interface in the Database
|
||||||
|
// definition. It implements the methods dealing with individual database
|
||||||
|
// connections and is used in all the builtin database types.
|
||||||
|
type ConnectionProducer interface {
|
||||||
|
Close() error
|
||||||
|
Initialize(map[string]interface{}, bool) error
|
||||||
|
Connection() (interface{}, error)
|
||||||
|
|
||||||
|
sync.Locker
|
||||||
|
}
|
136
plugins/helper/database/connutil/sql.go
Normal file
136
plugins/helper/database/connutil/sql.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package connutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// Import sql drivers
|
||||||
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/hashicorp/vault/helper/parseutil"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLConnectionProducer implements ConnectionProducer and provides a generic producer for most sql databases
|
||||||
|
type SQLConnectionProducer struct {
|
||||||
|
ConnectionURL string `json:"connection_url" structs:"connection_url" mapstructure:"connection_url"`
|
||||||
|
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"`
|
||||||
|
MaxConnectionLifetimeRaw interface{} `json:"max_connection_lifetime" structs:"max_connection_lifetime" mapstructure:"max_connection_lifetime"`
|
||||||
|
|
||||||
|
Type string
|
||||||
|
maxConnectionLifetime time.Duration
|
||||||
|
Initialized bool
|
||||||
|
db *sql.DB
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SQLConnectionProducer) Initialize(conf map[string]interface{}, verifyConnection bool) error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
err := mapstructure.Decode(conf, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.ConnectionURL) == 0 {
|
||||||
|
return fmt.Errorf("connection_url cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxOpenConnections == 0 {
|
||||||
|
c.MaxOpenConnections = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxIdleConnections == 0 {
|
||||||
|
c.MaxIdleConnections = c.MaxOpenConnections
|
||||||
|
}
|
||||||
|
if c.MaxIdleConnections > c.MaxOpenConnections {
|
||||||
|
c.MaxIdleConnections = c.MaxOpenConnections
|
||||||
|
}
|
||||||
|
if c.MaxConnectionLifetimeRaw == nil {
|
||||||
|
c.MaxConnectionLifetimeRaw = "0s"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.maxConnectionLifetime, err = parseutil.ParseDurationSecond(c.MaxConnectionLifetimeRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid max_connection_lifetime: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verifyConnection {
|
||||||
|
if _, err := c.Connection(); err != nil {
|
||||||
|
return fmt.Errorf("error initalizing connection: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.db.Ping(); err != nil {
|
||||||
|
return fmt.Errorf("error initalizing connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Initialized = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SQLConnectionProducer) Connection() (interface{}, error) {
|
||||||
|
// If we already have a DB, test it and return
|
||||||
|
if c.db != nil {
|
||||||
|
if err := c.db.Ping(); err == nil {
|
||||||
|
return c.db, nil
|
||||||
|
}
|
||||||
|
// If the ping was unsuccessful, close it and ignore errors as we'll be
|
||||||
|
// reestablishing anyways
|
||||||
|
c.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For mssql backend, switch to sqlserver instead
|
||||||
|
dbType := c.Type
|
||||||
|
if c.Type == "mssql" {
|
||||||
|
dbType = "sqlserver"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, attempt to make connection
|
||||||
|
conn := c.ConnectionURL
|
||||||
|
|
||||||
|
// Ensure timezone is set to UTC for all the conenctions
|
||||||
|
if strings.HasPrefix(conn, "postgres://") || strings.HasPrefix(conn, "postgresql://") {
|
||||||
|
if strings.Contains(conn, "?") {
|
||||||
|
conn += "&timezone=utc"
|
||||||
|
} else {
|
||||||
|
conn += "?timezone=utc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
c.db, err = sql.Open(dbType, conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set some connection pool settings. We don't need much of this,
|
||||||
|
// since the request rate shouldn't be high.
|
||||||
|
c.db.SetMaxOpenConns(c.MaxOpenConnections)
|
||||||
|
c.db.SetMaxIdleConns(c.MaxIdleConnections)
|
||||||
|
c.db.SetConnMaxLifetime(c.maxConnectionLifetime)
|
||||||
|
|
||||||
|
return c.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close attempts to close the connection
|
||||||
|
func (c *SQLConnectionProducer) Close() error {
|
||||||
|
// Grab the write lock
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
if c.db != nil {
|
||||||
|
c.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.db = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
37
plugins/helper/database/credsutil/cassandra.go
Normal file
37
plugins/helper/database/credsutil/cassandra.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package credsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
uuid "github.com/hashicorp/go-uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CassandraCredentialsProducer implements CredentialsProducer and provides an
|
||||||
|
// interface for cassandra databases to generate user information.
|
||||||
|
type CassandraCredentialsProducer struct{}
|
||||||
|
|
||||||
|
func (ccp *CassandraCredentialsProducer) GenerateUsername(displayName string) (string, error) {
|
||||||
|
userUUID, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
username := fmt.Sprintf("vault_%s_%s_%d", displayName, userUUID, time.Now().Unix())
|
||||||
|
username = strings.Replace(username, "-", "_", -1)
|
||||||
|
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ccp *CassandraCredentialsProducer) GeneratePassword() (string, error) {
|
||||||
|
password, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ccp *CassandraCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
12
plugins/helper/database/credsutil/credsutil.go
Normal file
12
plugins/helper/database/credsutil/credsutil.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package credsutil
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// CredentialsProducer can be used as an embeded interface in the Database
|
||||||
|
// definition. It implements the methods for generating user information for a
|
||||||
|
// particular database type and is used in all the builtin database types.
|
||||||
|
type CredentialsProducer interface {
|
||||||
|
GenerateUsername(displayName string) (string, error)
|
||||||
|
GeneratePassword() (string, error)
|
||||||
|
GenerateExpiration(ttl time.Time) (string, error)
|
||||||
|
}
|
43
plugins/helper/database/credsutil/sql.go
Normal file
43
plugins/helper/database/credsutil/sql.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package credsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
uuid "github.com/hashicorp/go-uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLCredentialsProducer implements CredentialsProducer and provides a generic credentials producer for most sql database types.
|
||||||
|
type SQLCredentialsProducer struct {
|
||||||
|
DisplayNameLen int
|
||||||
|
UsernameLen int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scp *SQLCredentialsProducer) GenerateUsername(displayName string) (string, error) {
|
||||||
|
if scp.DisplayNameLen > 0 && len(displayName) > scp.DisplayNameLen {
|
||||||
|
displayName = displayName[:scp.DisplayNameLen]
|
||||||
|
}
|
||||||
|
userUUID, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
username := fmt.Sprintf("v-%s-%s", displayName, userUUID)
|
||||||
|
if scp.UsernameLen > 0 && len(username) > scp.UsernameLen {
|
||||||
|
username = username[:scp.UsernameLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scp *SQLCredentialsProducer) GeneratePassword() (string, error) {
|
||||||
|
password, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scp *SQLCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) {
|
||||||
|
return ttl.Format("2006-01-02 15:04:05-0700"), nil
|
||||||
|
}
|
20
plugins/helper/database/dbutil/dbutil.go
Normal file
20
plugins/helper/database/dbutil/dbutil.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package dbutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEmptyCreationStatement = errors.New("empty creation statements")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query templates a query for us.
|
||||||
|
func QueryHelper(tpl string, data map[string]string) string {
|
||||||
|
for k, v := range data {
|
||||||
|
tpl = strings.Replace(tpl, fmt.Sprintf("{{%s}}", k), v, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tpl
|
||||||
|
}
|
31
plugins/serve.go
Normal file
31
plugins/serve.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve is used to start a plugin's RPC server. It takes an interface that must
|
||||||
|
// implement a known plugin interface to vault and an optional api.TLSConfig for
|
||||||
|
// use during the inital unwrap request to vault. The api config is particulary
|
||||||
|
// useful when vault is setup to require client cert checking.
|
||||||
|
func Serve(plugin interface{}, tlsConfig *api.TLSConfig) {
|
||||||
|
tlsProvider := pluginutil.VaultPluginTLSProvider(tlsConfig)
|
||||||
|
|
||||||
|
err := pluginutil.OptionallyEnableMlock()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p := plugin.(type) {
|
||||||
|
case dbplugin.Database:
|
||||||
|
dbplugin.Serve(p, tlsProvider)
|
||||||
|
default:
|
||||||
|
fmt.Println("Unsupported plugin type")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -330,6 +331,14 @@ type Core struct {
|
||||||
|
|
||||||
// uiEnabled indicates whether Vault Web UI is enabled or not
|
// uiEnabled indicates whether Vault Web UI is enabled or not
|
||||||
uiEnabled bool
|
uiEnabled bool
|
||||||
|
|
||||||
|
// pluginDirectory is the location vault will look for plugin binaries
|
||||||
|
pluginDirectory string
|
||||||
|
|
||||||
|
// pluginCatalog is used to manage plugin configurations
|
||||||
|
pluginCatalog *PluginCatalog
|
||||||
|
|
||||||
|
enableMlock bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoreConfig is used to parameterize a core
|
// CoreConfig is used to parameterize a core
|
||||||
|
@ -374,6 +383,8 @@ type CoreConfig struct {
|
||||||
|
|
||||||
EnableUI bool `json:"ui" structs:"ui" mapstructure:"ui"`
|
EnableUI bool `json:"ui" structs:"ui" mapstructure:"ui"`
|
||||||
|
|
||||||
|
PluginDirectory string `json:"plugin_directory" structs:"plugin_directory" mapstructure:"plugin_directory"`
|
||||||
|
|
||||||
ReloadFuncs *map[string][]ReloadFunc
|
ReloadFuncs *map[string][]ReloadFunc
|
||||||
ReloadFuncsLock *sync.RWMutex
|
ReloadFuncsLock *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
@ -430,6 +441,7 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
||||||
clusterName: conf.ClusterName,
|
clusterName: conf.ClusterName,
|
||||||
clusterListenerShutdownCh: make(chan struct{}),
|
clusterListenerShutdownCh: make(chan struct{}),
|
||||||
clusterListenerShutdownSuccessCh: make(chan struct{}),
|
clusterListenerShutdownSuccessCh: make(chan struct{}),
|
||||||
|
enableMlock: !conf.DisableMlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the physical backend in a cache layer if enabled and not already wrapped
|
// Wrap the physical backend in a cache layer if enabled and not already wrapped
|
||||||
|
@ -453,8 +465,15 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a new AES-GCM barrier
|
|
||||||
var err error
|
var err error
|
||||||
|
if conf.PluginDirectory != "" {
|
||||||
|
c.pluginDirectory, err = filepath.Abs(conf.PluginDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("core setup failed, could not verify plugin directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a new AES-GCM barrier
|
||||||
c.barrier, err = NewAESGCMBarrier(c.physical)
|
c.barrier, err = NewAESGCMBarrier(c.physical)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("barrier setup failed: %v", err)
|
return nil, fmt.Errorf("barrier setup failed: %v", err)
|
||||||
|
@ -1280,6 +1299,10 @@ func (c *Core) postUnseal() (retErr error) {
|
||||||
if err := c.setupAuditedHeadersConfig(); err != nil {
|
if err := c.setupAuditedHeadersConfig(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := c.setupPluginCatalog(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if c.ha != nil {
|
if c.ha != nil {
|
||||||
if err := c.startClusterListener(); err != nil {
|
if err := c.startClusterListener(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package vault
|
package vault
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/consts"
|
"github.com/hashicorp/vault/helper/consts"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -87,3 +90,49 @@ func (d dynamicSystemView) ReplicationState() consts.ReplicationState {
|
||||||
d.core.clusterParamsLock.RUnlock()
|
d.core.clusterParamsLock.RUnlock()
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResponseWrapData wraps the given data in a cubbyhole and returns the
|
||||||
|
// token used to unwrap.
|
||||||
|
func (d dynamicSystemView) ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) {
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.CreateOperation,
|
||||||
|
Path: "sys/wrapping/wrap",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &logical.Response{
|
||||||
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
||||||
|
TTL: ttl,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwt {
|
||||||
|
resp.WrapInfo.Format = "jwt"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.core.wrapInCubbyhole(req, resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.WrapInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupPlugin looks for a plugin with the given name in the plugin catalog. It
|
||||||
|
// returns a PluginRunner or an error if no plugin was found.
|
||||||
|
func (d dynamicSystemView) LookupPlugin(name string) (*pluginutil.PluginRunner, error) {
|
||||||
|
r, err := d.core.pluginCatalog.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("no plugin found with name: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MlockEnabled returns the configuration setting for enabling mlock on plugins.
|
||||||
|
func (d dynamicSystemView) MlockEnabled() bool {
|
||||||
|
return d.core.enableMlock
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/consts"
|
"github.com/hashicorp/vault/helper/consts"
|
||||||
"github.com/hashicorp/vault/helper/parseutil"
|
"github.com/hashicorp/vault/helper/parseutil"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/hashicorp/vault/logical/framework"
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
@ -62,6 +63,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen
|
||||||
"replication/reindex",
|
"replication/reindex",
|
||||||
"rotate",
|
"rotate",
|
||||||
"config/auditing/*",
|
"config/auditing/*",
|
||||||
|
"plugins/catalog/*",
|
||||||
"revoke-prefix/*",
|
"revoke-prefix/*",
|
||||||
"leases/revoke-prefix/*",
|
"leases/revoke-prefix/*",
|
||||||
"leases/revoke-force/*",
|
"leases/revoke-force/*",
|
||||||
|
@ -736,6 +738,48 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen
|
||||||
HelpSynopsis: strings.TrimSpace(sysHelp["audited-headers"][0]),
|
HelpSynopsis: strings.TrimSpace(sysHelp["audited-headers"][0]),
|
||||||
HelpDescription: strings.TrimSpace(sysHelp["audited-headers"][1]),
|
HelpDescription: strings.TrimSpace(sysHelp["audited-headers"][1]),
|
||||||
},
|
},
|
||||||
|
&framework.Path{
|
||||||
|
Pattern: "plugins/catalog/$",
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ListOperation: b.handlePluginCatalogList,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-catalog"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["plugin-catalog"][1]),
|
||||||
|
},
|
||||||
|
&framework.Path{
|
||||||
|
Pattern: "plugins/catalog/(?P<name>.+)",
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "The name of the plugin",
|
||||||
|
},
|
||||||
|
"sha_256": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `The SHA256 sum of the executable used in the
|
||||||
|
command field. This should be HEX encoded.`,
|
||||||
|
},
|
||||||
|
"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.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.UpdateOperation: b.handlePluginCatalogUpdate,
|
||||||
|
logical.DeleteOperation: b.handlePluginCatalogDelete,
|
||||||
|
logical.ReadOperation: b.handlePluginCatalogRead,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-catalog"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["plugin-catalog"][1]),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,6 +812,77 @@ func (b *SystemBackend) invalidate(key string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) handlePluginCatalogList(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
plugins, err := b.Core.pluginCatalog.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logical.ListResponse(plugins), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) handlePluginCatalogUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
pluginName := d.Get("name").(string)
|
||||||
|
if pluginName == "" {
|
||||||
|
return logical.ErrorResponse("missing plugin name"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256 := d.Get("sha_256").(string)
|
||||||
|
if sha256 == "" {
|
||||||
|
return logical.ErrorResponse("missing SHA-256 value"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
command := d.Get("command").(string)
|
||||||
|
if command == "" {
|
||||||
|
return logical.ErrorResponse("missing command value"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256Bytes, err := hex.DecodeString(sha256)
|
||||||
|
if err != nil {
|
||||||
|
return logical.ErrorResponse("Could not decode SHA-256 value from Hex"), err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.Core.pluginCatalog.Set(pluginName, command, sha256Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) handlePluginCatalogRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
pluginName := d.Get("name").(string)
|
||||||
|
if pluginName == "" {
|
||||||
|
return logical.ErrorResponse("missing plugin name"), nil
|
||||||
|
}
|
||||||
|
plugin, err := b.Core.pluginCatalog.Get(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plugin": plugin,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) handlePluginCatalogDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
pluginName := d.Get("name").(string)
|
||||||
|
if pluginName == "" {
|
||||||
|
return logical.ErrorResponse("missing plugin name"), nil
|
||||||
|
}
|
||||||
|
err := b.Core.pluginCatalog.Delete(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleAuditedHeaderUpdate creates or overwrites a header entry
|
// handleAuditedHeaderUpdate creates or overwrites a header entry
|
||||||
func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
header := d.Get("header").(string)
|
header := d.Get("header").(string)
|
||||||
|
@ -2074,7 +2189,7 @@ func (b *SystemBackend) handleWrappingRewrap(
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"response": response,
|
"response": response,
|
||||||
},
|
},
|
||||||
WrapInfo: &logical.ResponseWrapInfo{
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
||||||
TTL: time.Duration(creationTTL),
|
TTL: time.Duration(creationTTL),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -2524,7 +2639,23 @@ This path responds to the following HTTP methods.
|
||||||
"Lists the headers configured to be audited.",
|
"Lists the headers configured to be audited.",
|
||||||
`Returns a list of headers that have been configured to be audited.`,
|
`Returns a list of headers that have been configured to be audited.`,
|
||||||
},
|
},
|
||||||
|
"plugins/catalog": {
|
||||||
|
`Configures the plugins known to vault`,
|
||||||
|
`
|
||||||
|
This path responds to the following HTTP methods.
|
||||||
|
LIST /
|
||||||
|
Returns a list of names of configured plugins.
|
||||||
|
|
||||||
|
GET /<name>
|
||||||
|
Retrieve the metadata for the named plugin.
|
||||||
|
|
||||||
|
PUT /<name>
|
||||||
|
Add or update plugin.
|
||||||
|
|
||||||
|
DELETE /<name>
|
||||||
|
Delete the plugin with the given name.
|
||||||
|
`,
|
||||||
|
},
|
||||||
"leases": {
|
"leases": {
|
||||||
`View or list lease metadata.`,
|
`View or list lease metadata.`,
|
||||||
`
|
`
|
||||||
|
|
|
@ -2,6 +2,11 @@ package vault
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -9,6 +14,8 @@ import (
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
"github.com/hashicorp/vault/audit"
|
"github.com/hashicorp/vault/audit"
|
||||||
|
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
"github.com/hashicorp/vault/helper/salt"
|
"github.com/hashicorp/vault/helper/salt"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
@ -25,6 +32,7 @@ func TestSystemBackend_RootPaths(t *testing.T) {
|
||||||
"replication/reindex",
|
"replication/reindex",
|
||||||
"rotate",
|
"rotate",
|
||||||
"config/auditing/*",
|
"config/auditing/*",
|
||||||
|
"plugins/catalog/*",
|
||||||
"revoke-prefix/*",
|
"revoke-prefix/*",
|
||||||
"leases/revoke-prefix/*",
|
"leases/revoke-prefix/*",
|
||||||
"leases/revoke-force/*",
|
"leases/revoke-force/*",
|
||||||
|
@ -1543,3 +1551,92 @@ func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) {
|
||||||
}
|
}
|
||||||
return c, b, root
|
return c, b, root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) {
|
||||||
|
c, b, _ := testCoreSystemBackend(t)
|
||||||
|
// Bootstrap the pluginCatalog
|
||||||
|
sym, err := filepath.EvalSymlinks(os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
c.pluginCatalog.directory = sym
|
||||||
|
|
||||||
|
req := logical.TestRequest(t, logical.ListOperation, "plugins/catalog/")
|
||||||
|
resp, err := b.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Data["keys"].([]string)) != len(builtinplugins.Keys()) {
|
||||||
|
t.Fatalf("Wrong number of plugins, got %d, expected %d", len(resp.Data["keys"].([]string)), len(builtinplugins.Keys()))
|
||||||
|
}
|
||||||
|
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/mysql-database-plugin")
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedBuiltin := &pluginutil.PluginRunner{
|
||||||
|
Name: "mysql-database-plugin",
|
||||||
|
Builtin: true,
|
||||||
|
}
|
||||||
|
expectedBuiltin.BuiltinFactory, _ = builtinplugins.Get("mysql-database-plugin")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a plugin
|
||||||
|
file, err := ioutil.TempFile(os.TempDir(), "temp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
command := fmt.Sprintf("%s --test", filepath.Base(file.Name()))
|
||||||
|
req = logical.TestRequest(t, logical.UpdateOperation, "plugins/catalog/test-plugin")
|
||||||
|
req.Data["sha_256"] = hex.EncodeToString([]byte{'1'})
|
||||||
|
req.Data["command"] = command
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/test-plugin")
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete plugin
|
||||||
|
req = logical.TestRequest(t, logical.DeleteOperation, "plugins/catalog/test-plugin")
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "plugins/catalog/test-plugin")
|
||||||
|
resp, err = b.HandleRequest(req)
|
||||||
|
if resp != nil || err != nil {
|
||||||
|
t.Fatalf("expected nil response, plugin not deleted correctly got resp: %v, err: %v", resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
176
vault/plugin_catalog.go
Normal file
176
vault/plugin_catalog.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||||
|
"github.com/hashicorp/vault/helper/jsonutil"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pluginCatalogPath = "core/plugin-catalog/"
|
||||||
|
ErrDirectoryNotConfigured = errors.New("could not set plugin, plugin directory is not configured")
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginCatalog keeps a record of plugins known to vault. External plugins need
|
||||||
|
// to be registered to the catalog before they can be used in backends. Builtin
|
||||||
|
// plugins are automatically detected and included in the catalog.
|
||||||
|
type PluginCatalog struct {
|
||||||
|
catalogView *BarrierView
|
||||||
|
directory string
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) setupPluginCatalog() error {
|
||||||
|
c.pluginCatalog = &PluginCatalog{
|
||||||
|
catalogView: NewBarrierView(c.barrier, pluginCatalogPath),
|
||||||
|
directory: c.pluginDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a plugin with the specified name from the catalog. It first
|
||||||
|
// looks for external plugins with this name and then looks for builtin plugins.
|
||||||
|
// It returns a PluginRunner or an error if no plugin was found.
|
||||||
|
func (c *PluginCatalog) Get(name string) (*pluginutil.PluginRunner, error) {
|
||||||
|
c.lock.RLock()
|
||||||
|
defer c.lock.RUnlock()
|
||||||
|
|
||||||
|
// If the directory isn't set only look for builtin plugins.
|
||||||
|
if c.directory != "" {
|
||||||
|
// Look for external plugins in the barrier
|
||||||
|
out, err := c.catalogView.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve plugin \"%s\": %v", name, err)
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
entry := new(pluginutil.PluginRunner)
|
||||||
|
if err := jsonutil.DecodeJSON(out.Value, entry); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode plugin entry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepend the plugin directory to the command
|
||||||
|
entry.Command = filepath.Join(c.directory, entry.Command)
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Look for builtin plugins
|
||||||
|
if factory, ok := builtinplugins.Get(name); ok {
|
||||||
|
return &pluginutil.PluginRunner{
|
||||||
|
Name: name,
|
||||||
|
Builtin: true,
|
||||||
|
BuiltinFactory: factory,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set registers a new external plugin with the catalog, or updates an existing
|
||||||
|
// external plugin. It takes the name, command and SHA256 of the plugin.
|
||||||
|
func (c *PluginCatalog) Set(name, command string, sha256 []byte) error {
|
||||||
|
if c.directory == "" {
|
||||||
|
return ErrDirectoryNotConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
parts := strings.Split(command, " ")
|
||||||
|
|
||||||
|
// Best effort check to make sure the command isn't breaking out of the
|
||||||
|
// configured plugin directory.
|
||||||
|
commandFull := filepath.Join(c.directory, parts[0])
|
||||||
|
sym, err := filepath.EvalSymlinks(commandFull)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while validating the command path: %v", err)
|
||||||
|
}
|
||||||
|
symAbs, err := filepath.Abs(filepath.Dir(sym))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while validating the command path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if symAbs != c.directory {
|
||||||
|
return errors.New("can not execute files outside of configured plugin directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &pluginutil.PluginRunner{
|
||||||
|
Name: name,
|
||||||
|
Command: parts[0],
|
||||||
|
Args: parts[1:],
|
||||||
|
Sha256: sha256,
|
||||||
|
Builtin: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode plugin entry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logicalEntry := logical.StorageEntry{
|
||||||
|
Key: name,
|
||||||
|
Value: buf,
|
||||||
|
}
|
||||||
|
if err := c.catalogView.Put(&logicalEntry); err != nil {
|
||||||
|
return fmt.Errorf("failed to persist plugin entry: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete is used to remove an external plugin from the catalog. Builtin plugins
|
||||||
|
// can not be deleted.
|
||||||
|
func (c *PluginCatalog) Delete(name string) error {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
return c.catalogView.Delete(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a list of all the known plugin names. If an external and builtin
|
||||||
|
// plugin share the same name, only one instance of the name will be returned.
|
||||||
|
func (c *PluginCatalog) List() ([]string, error) {
|
||||||
|
c.lock.RLock()
|
||||||
|
defer c.lock.RUnlock()
|
||||||
|
|
||||||
|
// Collect keys for external plugins in the barrier.
|
||||||
|
keys, err := logical.CollectKeys(c.catalogView)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the keys for builtin plugins
|
||||||
|
builtinKeys := builtinplugins.Keys()
|
||||||
|
|
||||||
|
// Use a map to unique the two lists
|
||||||
|
mapKeys := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, plugin := range keys {
|
||||||
|
mapKeys[plugin] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, plugin := range builtinKeys {
|
||||||
|
mapKeys[plugin] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
retList := make([]string, len(mapKeys))
|
||||||
|
i := 0
|
||||||
|
for k := range mapKeys {
|
||||||
|
retList[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
// sort for consistent ordering of builtin pluings
|
||||||
|
sort.Strings(retList)
|
||||||
|
|
||||||
|
return retList, nil
|
||||||
|
}
|
176
vault/plugin_catalog_test.go
Normal file
176
vault/plugin_catalog_test.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||||
|
"github.com/hashicorp/vault/helper/pluginutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginCatalog_CRUD(t *testing.T) {
|
||||||
|
core, _, _ := TestCoreUnsealed(t)
|
||||||
|
|
||||||
|
sym, err := filepath.EvalSymlinks(os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
core.pluginCatalog.directory = sym
|
||||||
|
|
||||||
|
// Get builtin plugin
|
||||||
|
p, err := core.pluginCatalog.Get("mysql-database-plugin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedBuiltin := &pluginutil.PluginRunner{
|
||||||
|
Name: "mysql-database-plugin",
|
||||||
|
Builtin: true,
|
||||||
|
}
|
||||||
|
expectedBuiltin.BuiltinFactory, _ = builtinplugins.Get("mysql-database-plugin")
|
||||||
|
|
||||||
|
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", p, expectedBuiltin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a plugin, test overwriting a builtin plugin
|
||||||
|
file, err := ioutil.TempFile(os.TempDir(), "temp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
command := fmt.Sprintf("%s --test", filepath.Base(file.Name()))
|
||||||
|
err = core.pluginCatalog.Set("mysql-database-plugin", command, []byte{'1'})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the plugin
|
||||||
|
p, err = core.pluginCatalog.Get("mysql-database-plugin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &pluginutil.PluginRunner{
|
||||||
|
Name: "mysql-database-plugin",
|
||||||
|
Command: filepath.Join(sym, filepath.Base(file.Name())),
|
||||||
|
Args: []string{"--test"},
|
||||||
|
Sha256: []byte{'1'},
|
||||||
|
Builtin: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(p, expected) {
|
||||||
|
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", p, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the plugin
|
||||||
|
err = core.pluginCatalog.Delete("mysql-database-plugin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get builtin plugin
|
||||||
|
p, err = core.pluginCatalog.Get("mysql-database-plugin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedBuiltin = &pluginutil.PluginRunner{
|
||||||
|
Name: "mysql-database-plugin",
|
||||||
|
Builtin: true,
|
||||||
|
}
|
||||||
|
expectedBuiltin.BuiltinFactory, _ = builtinplugins.Get("mysql-database-plugin")
|
||||||
|
|
||||||
|
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", p, expectedBuiltin)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginCatalog_List(t *testing.T) {
|
||||||
|
core, _, _ := TestCoreUnsealed(t)
|
||||||
|
|
||||||
|
sym, err := filepath.EvalSymlinks(os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
core.pluginCatalog.directory = sym
|
||||||
|
|
||||||
|
// Get builtin plugins and sort them
|
||||||
|
builtinKeys := builtinplugins.Keys()
|
||||||
|
sort.Strings(builtinKeys)
|
||||||
|
|
||||||
|
// List only builtin plugins
|
||||||
|
plugins, err := core.pluginCatalog.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plugins) != len(builtinKeys) {
|
||||||
|
t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys), len(plugins))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range builtinKeys {
|
||||||
|
if !reflect.DeepEqual(plugins[i], p) {
|
||||||
|
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[i], p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a plugin, test overwriting a builtin plugin
|
||||||
|
file, err := ioutil.TempFile(os.TempDir(), "temp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
command := fmt.Sprintf("%s --test", filepath.Base(file.Name()))
|
||||||
|
err = core.pluginCatalog.Set("mysql-database-plugin", command, []byte{'1'})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set another plugin
|
||||||
|
err = core.pluginCatalog.Set("aaaaaaa", command, []byte{'1'})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the plugins
|
||||||
|
plugins, err = core.pluginCatalog.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plugins) != len(builtinKeys)+1 {
|
||||||
|
t.Fatalf("unexpected length of plugin list, expected %d, got %d", len(builtinKeys)+1, len(plugins))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the first plugin is the one we just created.
|
||||||
|
if !reflect.DeepEqual(plugins[0], "aaaaaaa") {
|
||||||
|
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[0], "aaaaaaa")
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the builtin pluings are correct
|
||||||
|
for i, p := range builtinKeys {
|
||||||
|
if !reflect.DeepEqual(plugins[i+1], p) {
|
||||||
|
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", plugins[i+1], p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/hashicorp/vault/helper/jsonutil"
|
"github.com/hashicorp/vault/helper/jsonutil"
|
||||||
"github.com/hashicorp/vault/helper/policyutil"
|
"github.com/hashicorp/vault/helper/policyutil"
|
||||||
"github.com/hashicorp/vault/helper/strutil"
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
|
"github.com/hashicorp/vault/helper/wrapping"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -216,7 +217,7 @@ func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, r
|
||||||
}
|
}
|
||||||
|
|
||||||
if wrapTTL > 0 {
|
if wrapTTL > 0 {
|
||||||
resp.WrapInfo = &logical.ResponseWrapInfo{
|
resp.WrapInfo = &wrapping.ResponseWrapInfo{
|
||||||
TTL: wrapTTL,
|
TTL: wrapTTL,
|
||||||
Format: wrapFormat,
|
Format: wrapFormat,
|
||||||
}
|
}
|
||||||
|
@ -361,7 +362,7 @@ func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *log
|
||||||
}
|
}
|
||||||
|
|
||||||
if wrapTTL > 0 {
|
if wrapTTL > 0 {
|
||||||
resp.WrapInfo = &logical.ResponseWrapInfo{
|
resp.WrapInfo = &wrapping.ResponseWrapInfo{
|
||||||
TTL: wrapTTL,
|
TTL: wrapTTL,
|
||||||
Format: wrapFormat,
|
Format: wrapFormat,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,12 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -293,6 +296,45 @@ func TestKeyCopy(key []byte) []byte {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDynamicSystemView(c *Core) *dynamicSystemView {
|
||||||
|
me := &MountEntry{
|
||||||
|
Config: MountConfig{
|
||||||
|
DefaultLeaseTTL: 24 * time.Hour,
|
||||||
|
MaxLeaseTTL: 2 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dynamicSystemView{c, me}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTestPlugin(t testing.TB, c *Core, name, testFunc string) {
|
||||||
|
file, err := os.Open(os.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
_, err = io.Copy(hash, file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := hash.Sum(nil)
|
||||||
|
c.pluginCatalog.directory, err = filepath.EvalSymlinks(os.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c.pluginCatalog.directory = filepath.Dir(c.pluginCatalog.directory)
|
||||||
|
|
||||||
|
command := fmt.Sprintf("%s --test.run=%s", filepath.Base(os.Args[0]), testFunc)
|
||||||
|
err = c.pluginCatalog.Set(name, command, sum)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var testLogicalBackends = map[string]logical.Factory{}
|
var testLogicalBackends = map[string]logical.Factory{}
|
||||||
|
|
||||||
// Starts the test server which responds to SSH authentication.
|
// Starts the test server which responds to SSH authentication.
|
||||||
|
|
353
vendor/github.com/hashicorp/go-plugin/LICENSE
generated
vendored
Normal file
353
vendor/github.com/hashicorp/go-plugin/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
Mozilla Public License, version 2.0
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
1.1. “Contributor”
|
||||||
|
|
||||||
|
means each individual or legal entity that creates, contributes to the
|
||||||
|
creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. “Contributor Version”
|
||||||
|
|
||||||
|
means the combination of the Contributions of others (if any) used by a
|
||||||
|
Contributor and that particular Contributor’s Contribution.
|
||||||
|
|
||||||
|
1.3. “Contribution”
|
||||||
|
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. “Covered Software”
|
||||||
|
|
||||||
|
means Source Code Form to which the initial Contributor has attached the
|
||||||
|
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||||
|
Modifications of such Source Code Form, in each case including portions
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
1.5. “Incompatible With Secondary Licenses”
|
||||||
|
means
|
||||||
|
|
||||||
|
a. that the initial Contributor has attached the notice described in
|
||||||
|
Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
b. that the Covered Software was made available under the terms of version
|
||||||
|
1.1 or earlier of the License, but not also under the terms of a
|
||||||
|
Secondary License.
|
||||||
|
|
||||||
|
1.6. “Executable Form”
|
||||||
|
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. “Larger Work”
|
||||||
|
|
||||||
|
means a work that combines Covered Software with other material, in a separate
|
||||||
|
file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. “License”
|
||||||
|
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. “Licensable”
|
||||||
|
|
||||||
|
means having the right to grant, to the maximum extent possible, whether at the
|
||||||
|
time of the initial grant or subsequently, any and all of the rights conveyed by
|
||||||
|
this License.
|
||||||
|
|
||||||
|
1.10. “Modifications”
|
||||||
|
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
a. any file in Source Code Form that results from an addition to, deletion
|
||||||
|
from, or modification of the contents of Covered Software; or
|
||||||
|
|
||||||
|
b. any new file in Source Code Form that contains any Covered Software.
|
||||||
|
|
||||||
|
1.11. “Patent Claims” of a Contributor
|
||||||
|
|
||||||
|
means any patent claim(s), including without limitation, method, process,
|
||||||
|
and apparatus claims, in any patent Licensable by such Contributor that
|
||||||
|
would be infringed, but for the grant of the License, by the making,
|
||||||
|
using, selling, offering for sale, having made, import, or transfer of
|
||||||
|
either its Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
1.12. “Secondary License”
|
||||||
|
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||||
|
General Public License, Version 2.1, the GNU Affero General Public
|
||||||
|
License, Version 3.0, or any later versions of those licenses.
|
||||||
|
|
||||||
|
1.13. “Source Code Form”
|
||||||
|
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. “You” (or “Your”)
|
||||||
|
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, “You” includes any entity that controls, is
|
||||||
|
controlled by, or is under common control with You. For purposes of this
|
||||||
|
definition, “control” means (a) the power, direct or indirect, to cause
|
||||||
|
the direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||||
|
outstanding shares or beneficial ownership of such entity.
|
||||||
|
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
a. under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or as
|
||||||
|
part of a Larger Work; and
|
||||||
|
|
||||||
|
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||||
|
sale, have made, import, and otherwise transfer either its Contributions
|
||||||
|
or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution become
|
||||||
|
effective for each Contribution on the date the Contributor first distributes
|
||||||
|
such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under this
|
||||||
|
License. No additional rights or licenses will be implied from the distribution
|
||||||
|
or licensing of Covered Software under this License. Notwithstanding Section
|
||||||
|
2.1(b) above, no patent license is granted by a Contributor:
|
||||||
|
|
||||||
|
a. for any code that a Contributor has removed from Covered Software; or
|
||||||
|
|
||||||
|
b. for infringements caused by: (i) Your and any other third party’s
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
c. under Patent Claims infringed by Covered Software in the absence of its
|
||||||
|
Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks, or
|
||||||
|
logos of any Contributor (except as may be necessary to comply with the
|
||||||
|
notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this License
|
||||||
|
(see Section 10.2) or under the terms of a Secondary License (if permitted
|
||||||
|
under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its Contributions
|
||||||
|
are its original creation(s) or it has sufficient rights to grant the
|
||||||
|
rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under applicable
|
||||||
|
copyright doctrines of fair use, fair dealing, or other equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||||
|
Section 2.1.
|
||||||
|
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under the
|
||||||
|
terms of this License. You must inform recipients that the Source Code Form
|
||||||
|
of the Covered Software is governed by the terms of this License, and how
|
||||||
|
they can obtain a copy of this License. You may not attempt to alter or
|
||||||
|
restrict the recipients’ rights in the Source Code Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
a. such Covered Software must also be made available in Source Code Form,
|
||||||
|
as described in Section 3.1, and You must inform recipients of the
|
||||||
|
Executable Form how they can obtain a copy of such Source Code Form by
|
||||||
|
reasonable means in a timely manner, at a charge no more than the cost
|
||||||
|
of distribution to the recipient; and
|
||||||
|
|
||||||
|
b. You may distribute such Executable Form under the terms of this License,
|
||||||
|
or sublicense it under different terms, provided that the license for
|
||||||
|
the Executable Form does not attempt to limit or alter the recipients’
|
||||||
|
rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for the
|
||||||
|
Covered Software. If the Larger Work is a combination of Covered Software
|
||||||
|
with a work governed by one or more Secondary Licenses, and the Covered
|
||||||
|
Software is not Incompatible With Secondary Licenses, this License permits
|
||||||
|
You to additionally distribute such Covered Software under the terms of
|
||||||
|
such Secondary License(s), so that the recipient of the Larger Work may, at
|
||||||
|
their option, further distribute the Covered Software under the terms of
|
||||||
|
either this License or such Secondary License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices (including
|
||||||
|
copyright notices, patent notices, disclaimers of warranty, or limitations
|
||||||
|
of liability) contained within the Source Code Form of the Covered
|
||||||
|
Software, except that You may alter any license notices to the extent
|
||||||
|
required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on behalf
|
||||||
|
of any Contributor. You must make it absolutely clear that any such
|
||||||
|
warranty, support, indemnity, or liability obligation is offered by You
|
||||||
|
alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this License
|
||||||
|
with respect to some or all of the Covered Software due to statute, judicial
|
||||||
|
order, or regulation then You must: (a) comply with the terms of this License
|
||||||
|
to the maximum extent possible; and (b) describe the limitations and the code
|
||||||
|
they affect. Such description must be placed in a text file included with all
|
||||||
|
distributions of the Covered Software under this License. Except to the
|
||||||
|
extent prohibited by statute or regulation, such description must be
|
||||||
|
sufficiently detailed for a recipient of ordinary skill to be able to
|
||||||
|
understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically if You
|
||||||
|
fail to comply with any of its terms. However, if You become compliant,
|
||||||
|
then the rights granted under this License from a particular Contributor
|
||||||
|
are reinstated (a) provisionally, unless and until such Contributor
|
||||||
|
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
|
||||||
|
if such Contributor fails to notify You of the non-compliance by some
|
||||||
|
reasonable means prior to 60 days after You have come back into compliance.
|
||||||
|
Moreover, Your grants from a particular Contributor are reinstated on an
|
||||||
|
ongoing basis if such Contributor notifies You of the non-compliance by
|
||||||
|
some reasonable means, this is the first time You have received notice of
|
||||||
|
non-compliance with this License from such Contributor, and You become
|
||||||
|
compliant prior to 30 days after Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions, counter-claims,
|
||||||
|
and cross-claims) alleging that a Contributor Version directly or
|
||||||
|
indirectly infringes any patent, then the rights granted to You by any and
|
||||||
|
all Contributors for the Covered Software under Section 2.1 of this License
|
||||||
|
shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||||
|
license agreements (excluding distributors and resellers) which have been
|
||||||
|
validly granted by You or Your distributors under this License prior to
|
||||||
|
termination shall survive termination.
|
||||||
|
|
||||||
|
6. Disclaimer of Warranty
|
||||||
|
|
||||||
|
Covered Software is provided under this License on an “as is” basis, without
|
||||||
|
warranty of any kind, either expressed, implied, or statutory, including,
|
||||||
|
without limitation, warranties that the Covered Software is free of defects,
|
||||||
|
merchantable, fit for a particular purpose or non-infringing. The entire
|
||||||
|
risk as to the quality and performance of the Covered Software is with You.
|
||||||
|
Should any Covered Software prove defective in any respect, You (not any
|
||||||
|
Contributor) assume the cost of any necessary servicing, repair, or
|
||||||
|
correction. This disclaimer of warranty constitutes an essential part of this
|
||||||
|
License. No use of any Covered Software is authorized under this License
|
||||||
|
except under this disclaimer.
|
||||||
|
|
||||||
|
7. Limitation of Liability
|
||||||
|
|
||||||
|
Under no circumstances and under no legal theory, whether tort (including
|
||||||
|
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||||
|
distributes Covered Software as permitted above, be liable to You for any
|
||||||
|
direct, indirect, special, incidental, or consequential damages of any
|
||||||
|
character including, without limitation, damages for lost profits, loss of
|
||||||
|
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses, even if such party shall have been
|
||||||
|
informed of the possibility of such damages. This limitation of liability
|
||||||
|
shall not apply to liability for death or personal injury resulting from such
|
||||||
|
party’s negligence to the extent applicable law prohibits such limitation.
|
||||||
|
Some jurisdictions do not allow the exclusion or limitation of incidental or
|
||||||
|
consequential damages, so this exclusion and limitation may not apply to You.
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the courts of
|
||||||
|
a jurisdiction where the defendant maintains its principal place of business
|
||||||
|
and such litigation shall be governed by laws of that jurisdiction, without
|
||||||
|
reference to its conflict-of-law provisions. Nothing in this Section shall
|
||||||
|
prevent a party’s ability to bring cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject matter
|
||||||
|
hereof. If any provision of this License is held to be unenforceable, such
|
||||||
|
provision shall be reformed only to the extent necessary to make it
|
||||||
|
enforceable. Any law or regulation which provides that the language of a
|
||||||
|
contract shall be construed against the drafter shall not be used to construe
|
||||||
|
this License against a Contributor.
|
||||||
|
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version of
|
||||||
|
the License under which You originally received the Covered Software, or
|
||||||
|
under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a modified
|
||||||
|
version of this License if you rename the license and remove any
|
||||||
|
references to the name of the license steward (except to note that such
|
||||||
|
modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
|
||||||
|
This Source Code Form is subject to the
|
||||||
|
terms of the Mozilla Public License, v.
|
||||||
|
2.0. If a copy of the MPL was not
|
||||||
|
distributed with this file, You can
|
||||||
|
obtain one at
|
||||||
|
http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular file, then
|
||||||
|
You may include the notice in a location (such as a LICENSE file in a relevant
|
||||||
|
directory) where a recipient would be likely to look for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||||
|
|
||||||
|
This Source Code Form is “Incompatible
|
||||||
|
With Secondary Licenses”, as defined by
|
||||||
|
the Mozilla Public License, v. 2.0.
|
161
vendor/github.com/hashicorp/go-plugin/README.md
generated
vendored
Normal file
161
vendor/github.com/hashicorp/go-plugin/README.md
generated
vendored
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# Go Plugin System over RPC
|
||||||
|
|
||||||
|
`go-plugin` is a Go (golang) plugin system over RPC. It is the plugin system
|
||||||
|
that has been in use by HashiCorp tooling for over 3 years. While initially
|
||||||
|
created for [Packer](https://www.packer.io), it has since been used by
|
||||||
|
[Terraform](https://www.terraform.io) and [Otto](https://www.ottoproject.io),
|
||||||
|
with plans to also use it for [Nomad](https://www.nomadproject.io) and
|
||||||
|
[Vault](https://www.vaultproject.io).
|
||||||
|
|
||||||
|
While the plugin system is over RPC, it is currently only designed to work
|
||||||
|
over a local [reliable] network. Plugins over a real network are not supported
|
||||||
|
and will lead to unexpected behavior.
|
||||||
|
|
||||||
|
This plugin system has been used on millions of machines across many different
|
||||||
|
projects and has proven to be battle hardened and ready for production use.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The HashiCorp plugin system supports a number of features:
|
||||||
|
|
||||||
|
**Plugins are Go interface implementations.** This makes writing and consuming
|
||||||
|
plugins feel very natural. To a plugin author: you just implement an
|
||||||
|
interface as if it were going to run in the same process. For a plugin user:
|
||||||
|
you just use and call functions on an interface as if it were in the same
|
||||||
|
process. This plugin system handles the communication in between.
|
||||||
|
|
||||||
|
**Complex arguments and return values are supported.** This library
|
||||||
|
provides APIs for handling complex arguments and return values such
|
||||||
|
as interfaces, `io.Reader/Writer`, etc. We do this by giving you a library
|
||||||
|
(`MuxBroker`) for creating new connections between the client/server to
|
||||||
|
serve additional interfaces or transfer raw data.
|
||||||
|
|
||||||
|
**Bidirectional communication.** Because the plugin system supports
|
||||||
|
complex arguments, the host process can send it interface implementations
|
||||||
|
and the plugin can call back into the host process.
|
||||||
|
|
||||||
|
**Built-in Logging.** Any plugins that use the `log` standard library
|
||||||
|
will have log data automatically sent to the host process. The host
|
||||||
|
process will mirror this output prefixed with the path to the plugin
|
||||||
|
binary. This makes debugging with plugins simple.
|
||||||
|
|
||||||
|
**Protocol Versioning.** A very basic "protocol version" is supported that
|
||||||
|
can be incremented to invalidate any previous plugins. This is useful when
|
||||||
|
interface signatures are changing, protocol level changes are necessary,
|
||||||
|
etc. When a protocol version is incompatible, a human friendly error
|
||||||
|
message is shown to the end user.
|
||||||
|
|
||||||
|
**Stdout/Stderr Syncing.** While plugins are subprocesses, they can continue
|
||||||
|
to use stdout/stderr as usual and the output will get mirrored back to
|
||||||
|
the host process. The host process can control what `io.Writer` these
|
||||||
|
streams go to to prevent this from happening.
|
||||||
|
|
||||||
|
**TTY Preservation.** Plugin subprocesses are connected to the identical
|
||||||
|
stdin file descriptor as the host process, allowing software that requires
|
||||||
|
a TTY to work. For example, a plugin can execute `ssh` and even though there
|
||||||
|
are multiple subprocesses and RPC happening, it will look and act perfectly
|
||||||
|
to the end user.
|
||||||
|
|
||||||
|
**Host upgrade while a plugin is running.** Plugins can be "reattached"
|
||||||
|
so that the host process can be upgraded while the plugin is still running.
|
||||||
|
This requires the host/plugin to know this is possible and daemonize
|
||||||
|
properly. `NewClient` takes a `ReattachConfig` to determine if and how to
|
||||||
|
reattach.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The HashiCorp plugin system works by launching subprocesses and communicating
|
||||||
|
over RPC (using standard `net/rpc`). A single connection is made between
|
||||||
|
any plugin and the host process, and we use a
|
||||||
|
[connection multiplexing](https://github.com/hashicorp/yamux)
|
||||||
|
library to multiplex any other connections on top.
|
||||||
|
|
||||||
|
This architecture has a number of benefits:
|
||||||
|
|
||||||
|
* Plugins can't crash your host process: A panic in a plugin doesn't
|
||||||
|
panic the plugin user.
|
||||||
|
|
||||||
|
* Plugins are very easy to write: just write a Go application and `go build`.
|
||||||
|
Theoretically you could also use another language as long as it can
|
||||||
|
communicate the Go `net/rpc` protocol but this hasn't yet been tried.
|
||||||
|
|
||||||
|
* Plugins are very easy to install: just put the binary in a location where
|
||||||
|
the host will find it (depends on the host but this library also provides
|
||||||
|
helpers), and the plugin host handles the rest.
|
||||||
|
|
||||||
|
* Plugins can be relatively secure: The plugin only has access to the
|
||||||
|
interfaces and args given to it, not to the entire memory space of the
|
||||||
|
process. More security features are planned (see the coming soon section
|
||||||
|
below).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use the plugin system, you must take the following steps. These are
|
||||||
|
high-level steps that must be done. Examples are available in the
|
||||||
|
`examples/` directory.
|
||||||
|
|
||||||
|
1. Choose the interface(s) you want to expose for plugins.
|
||||||
|
|
||||||
|
2. For each interface, implement an implementation of that interface
|
||||||
|
that communicates over an `*rpc.Client` (from the standard `net/rpc`
|
||||||
|
package) for every function call. Likewise, implement the RPC server
|
||||||
|
struct this communicates to which is then communicating to a real,
|
||||||
|
concrete implementation.
|
||||||
|
|
||||||
|
3. Create a `Plugin` implementation that knows how to create the RPC
|
||||||
|
client/server for a given plugin type.
|
||||||
|
|
||||||
|
4. Plugin authors call `plugin.Serve` to serve a plugin from the
|
||||||
|
`main` function.
|
||||||
|
|
||||||
|
5. Plugin users use `plugin.Client` to launch a subprocess and request
|
||||||
|
an interface implementation over RPC.
|
||||||
|
|
||||||
|
That's it! In practice, step 2 is the most tedious and time consuming step.
|
||||||
|
Even so, it isn't very difficult and you can see examples in the `examples/`
|
||||||
|
directory as well as throughout our various open source projects.
|
||||||
|
|
||||||
|
For complete API documentation, see [GoDoc](https://godoc.org/github.com/hashicorp/go-plugin).
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Our plugin system is constantly evolving. As we use the plugin system for
|
||||||
|
new projects or for new features in existing projects, we constantly find
|
||||||
|
improvements we can make.
|
||||||
|
|
||||||
|
At this point in time, the roadmap for the plugin system is:
|
||||||
|
|
||||||
|
**Cryptographically Secure Plugins.** We'll implement signing plugins
|
||||||
|
and loading signed plugins in order to allow Vault to make use of multi-process
|
||||||
|
in a secure way.
|
||||||
|
|
||||||
|
**Semantic Versioning.** Plugins will be able to implement a semantic version.
|
||||||
|
This plugin system will give host processes a system for constraining
|
||||||
|
versions. This is in addition to the protocol versioning already present
|
||||||
|
which is more for larger underlying changes.
|
||||||
|
|
||||||
|
**Plugin fetching.** We will integrate with [go-getter](https://github.com/hashicorp/go-getter)
|
||||||
|
to support automatic download + install of plugins. Paired with cryptographically
|
||||||
|
secure plugins (above), we can make this a safe operation for an amazing
|
||||||
|
user experience.
|
||||||
|
|
||||||
|
## What About Shared Libraries?
|
||||||
|
|
||||||
|
When we started using plugins (late 2012, early 2013), plugins over RPC
|
||||||
|
were the only option since Go didn't support dynamic library loading. Today,
|
||||||
|
Go still doesn't support dynamic library loading, but they do intend to.
|
||||||
|
Since 2012, our plugin system has stabilized from millions of users using it,
|
||||||
|
and has many benefits we've come to value greatly.
|
||||||
|
|
||||||
|
For example, we intend to use this plugin system in
|
||||||
|
[Vault](https://www.vaultproject.io), and dynamic library loading will
|
||||||
|
simply never be acceptable in Vault for security reasons. That is an extreme
|
||||||
|
example, but we believe our library system has more upsides than downsides
|
||||||
|
over dynamic library loading and since we've had it built and tested for years,
|
||||||
|
we'll likely continue to use it.
|
||||||
|
|
||||||
|
Shared libraries have one major advantage over our system which is much
|
||||||
|
higher performance. In real world scenarios across our various tools,
|
||||||
|
we've never required any more performance out of our plugin system and it
|
||||||
|
has seen very high throughput, so this isn't a concern for us at the moment.
|
||||||
|
|
666
vendor/github.com/hashicorp/go-plugin/client.go
generated
vendored
Normal file
666
vendor/github.com/hashicorp/go-plugin/client.go
generated
vendored
Normal file
|
@ -0,0 +1,666 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/subtle"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// If this is 1, then we've called CleanupClients. This can be used
|
||||||
|
// by plugin RPC implementations to change error behavior since you
|
||||||
|
// can expected network connection errors at this point. This should be
|
||||||
|
// read by using sync/atomic.
|
||||||
|
var Killed uint32 = 0
|
||||||
|
|
||||||
|
// This is a slice of the "managed" clients which are cleaned up when
|
||||||
|
// calling Cleanup
|
||||||
|
var managedClients = make([]*Client, 0, 5)
|
||||||
|
var managedClientsLock sync.Mutex
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
var (
|
||||||
|
// ErrProcessNotFound is returned when a client is instantiated to
|
||||||
|
// reattach to an existing process and it isn't found.
|
||||||
|
ErrProcessNotFound = errors.New("Reattachment process not found")
|
||||||
|
|
||||||
|
// ErrChecksumsDoNotMatch is returned when binary's checksum doesn't match
|
||||||
|
// the one provided in the SecureConfig.
|
||||||
|
ErrChecksumsDoNotMatch = errors.New("checksums did not match")
|
||||||
|
|
||||||
|
// ErrSecureNoChecksum is returned when an empty checksum is provided to the
|
||||||
|
// SecureConfig.
|
||||||
|
ErrSecureConfigNoChecksum = errors.New("no checksum provided")
|
||||||
|
|
||||||
|
// ErrSecureNoHash is returned when a nil Hash object is provided to the
|
||||||
|
// SecureConfig.
|
||||||
|
ErrSecureConfigNoHash = errors.New("no hash implementation provided")
|
||||||
|
|
||||||
|
// ErrSecureConfigAndReattach is returned when both Reattach and
|
||||||
|
// SecureConfig are set.
|
||||||
|
ErrSecureConfigAndReattach = errors.New("only one of Reattach or SecureConfig can be set")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client handles the lifecycle of a plugin application. It launches
|
||||||
|
// plugins, connects to them, dispenses interface implementations, and handles
|
||||||
|
// killing the process.
|
||||||
|
//
|
||||||
|
// Plugin hosts should use one Client for each plugin executable. To
|
||||||
|
// dispense a plugin type, use the `Client.Client` function, and then
|
||||||
|
// cal `Dispense`. This awkward API is mostly historical but is used to split
|
||||||
|
// the client that deals with subprocess management and the client that
|
||||||
|
// does RPC management.
|
||||||
|
//
|
||||||
|
// See NewClient and ClientConfig for using a Client.
|
||||||
|
type Client struct {
|
||||||
|
config *ClientConfig
|
||||||
|
exited bool
|
||||||
|
doneLogging chan struct{}
|
||||||
|
l sync.Mutex
|
||||||
|
address net.Addr
|
||||||
|
process *os.Process
|
||||||
|
client *RPCClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfig is the configuration used to initialize a new
|
||||||
|
// plugin client. After being used to initialize a plugin client,
|
||||||
|
// that configuration must not be modified again.
|
||||||
|
type ClientConfig struct {
|
||||||
|
// HandshakeConfig is the configuration that must match servers.
|
||||||
|
HandshakeConfig
|
||||||
|
|
||||||
|
// Plugins are the plugins that can be consumed.
|
||||||
|
Plugins map[string]Plugin
|
||||||
|
|
||||||
|
// One of the following must be set, but not both.
|
||||||
|
//
|
||||||
|
// Cmd is the unstarted subprocess for starting the plugin. If this is
|
||||||
|
// set, then the Client starts the plugin process on its own and connects
|
||||||
|
// to it.
|
||||||
|
//
|
||||||
|
// Reattach is configuration for reattaching to an existing plugin process
|
||||||
|
// that is already running. This isn't common.
|
||||||
|
Cmd *exec.Cmd
|
||||||
|
Reattach *ReattachConfig
|
||||||
|
|
||||||
|
// SecureConfig is configuration for verifying the integrity of the
|
||||||
|
// executable. It can not be used with Reattach.
|
||||||
|
SecureConfig *SecureConfig
|
||||||
|
|
||||||
|
// TLSConfig is used to enable TLS on the RPC client.
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
|
||||||
|
// Managed represents if the client should be managed by the
|
||||||
|
// plugin package or not. If true, then by calling CleanupClients,
|
||||||
|
// it will automatically be cleaned up. Otherwise, the client
|
||||||
|
// user is fully responsible for making sure to Kill all plugin
|
||||||
|
// clients. By default the client is _not_ managed.
|
||||||
|
Managed bool
|
||||||
|
|
||||||
|
// The minimum and maximum port to use for communicating with
|
||||||
|
// the subprocess. If not set, this defaults to 10,000 and 25,000
|
||||||
|
// respectively.
|
||||||
|
MinPort, MaxPort uint
|
||||||
|
|
||||||
|
// StartTimeout is the timeout to wait for the plugin to say it
|
||||||
|
// has started successfully.
|
||||||
|
StartTimeout time.Duration
|
||||||
|
|
||||||
|
// If non-nil, then the stderr of the client will be written to here
|
||||||
|
// (as well as the log). This is the original os.Stderr of the subprocess.
|
||||||
|
// This isn't the output of synced stderr.
|
||||||
|
Stderr io.Writer
|
||||||
|
|
||||||
|
// SyncStdout, SyncStderr can be set to override the
|
||||||
|
// respective os.Std* values in the plugin. Care should be taken to
|
||||||
|
// avoid races here. If these are nil, then this will automatically be
|
||||||
|
// hooked up to os.Stdin, Stdout, and Stderr, respectively.
|
||||||
|
//
|
||||||
|
// If the default values (nil) are used, then this package will not
|
||||||
|
// sync any of these streams.
|
||||||
|
SyncStdout io.Writer
|
||||||
|
SyncStderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReattachConfig is used to configure a client to reattach to an
|
||||||
|
// already-running plugin process. You can retrieve this information by
|
||||||
|
// calling ReattachConfig on Client.
|
||||||
|
type ReattachConfig struct {
|
||||||
|
Addr net.Addr
|
||||||
|
Pid int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureConfig is used to configure a client to verify the integrity of an
|
||||||
|
// executable before running. It does this by verifying the checksum is
|
||||||
|
// expected. Hash is used to specify the hashing method to use when checksumming
|
||||||
|
// the file. The configuration is verified by the client by calling the
|
||||||
|
// SecureConfig.Check() function.
|
||||||
|
//
|
||||||
|
// The host process should ensure the checksum was provided by a trusted and
|
||||||
|
// authoritative source. The binary should be installed in such a way that it
|
||||||
|
// can not be modified by an unauthorized user between the time of this check
|
||||||
|
// and the time of execution.
|
||||||
|
type SecureConfig struct {
|
||||||
|
Checksum []byte
|
||||||
|
Hash hash.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check takes the filepath to an executable and returns true if the checksum of
|
||||||
|
// the file matches the checksum provided in the SecureConfig.
|
||||||
|
func (s *SecureConfig) Check(filePath string) (bool, error) {
|
||||||
|
if len(s.Checksum) == 0 {
|
||||||
|
return false, ErrSecureConfigNoChecksum
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Hash == nil {
|
||||||
|
return false, ErrSecureConfigNoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(s.Hash, file)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := s.Hash.Sum(nil)
|
||||||
|
|
||||||
|
return subtle.ConstantTimeCompare(sum, s.Checksum) == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This makes sure all the managed subprocesses are killed and properly
|
||||||
|
// logged. This should be called before the parent process running the
|
||||||
|
// plugins exits.
|
||||||
|
//
|
||||||
|
// This must only be called _once_.
|
||||||
|
func CleanupClients() {
|
||||||
|
// Set the killed to true so that we don't get unexpected panics
|
||||||
|
atomic.StoreUint32(&Killed, 1)
|
||||||
|
|
||||||
|
// Kill all the managed clients in parallel and use a WaitGroup
|
||||||
|
// to wait for them all to finish up.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
managedClientsLock.Lock()
|
||||||
|
for _, client := range managedClients {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(client *Client) {
|
||||||
|
client.Kill()
|
||||||
|
wg.Done()
|
||||||
|
}(client)
|
||||||
|
}
|
||||||
|
managedClientsLock.Unlock()
|
||||||
|
|
||||||
|
log.Println("[DEBUG] plugin: waiting for all plugin processes to complete...")
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new plugin client which manages the lifecycle of an external
|
||||||
|
// plugin and gets the address for the RPC connection.
|
||||||
|
//
|
||||||
|
// The client must be cleaned up at some point by calling Kill(). If
|
||||||
|
// the client is a managed client (created with NewManagedClient) you
|
||||||
|
// can just call CleanupClients at the end of your program and they will
|
||||||
|
// be properly cleaned.
|
||||||
|
func NewClient(config *ClientConfig) (c *Client) {
|
||||||
|
if config.MinPort == 0 && config.MaxPort == 0 {
|
||||||
|
config.MinPort = 10000
|
||||||
|
config.MaxPort = 25000
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartTimeout == 0 {
|
||||||
|
config.StartTimeout = 1 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Stderr == nil {
|
||||||
|
config.Stderr = ioutil.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SyncStdout == nil {
|
||||||
|
config.SyncStdout = ioutil.Discard
|
||||||
|
}
|
||||||
|
if config.SyncStderr == nil {
|
||||||
|
config.SyncStderr = ioutil.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
c = &Client{config: config}
|
||||||
|
if config.Managed {
|
||||||
|
managedClientsLock.Lock()
|
||||||
|
managedClients = append(managedClients, c)
|
||||||
|
managedClientsLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an RPC client for the plugin.
|
||||||
|
//
|
||||||
|
// Subsequent calls to this will return the same RPC client.
|
||||||
|
func (c *Client) Client() (*RPCClient, error) {
|
||||||
|
addr, err := c.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
if c.client != nil {
|
||||||
|
return c.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the client
|
||||||
|
conn, err := net.Dial(addr.Network(), addr.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||||
|
// Make sure to set keep alive so that the connection doesn't die
|
||||||
|
tcpConn.SetKeepAlive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.TLSConfig != nil {
|
||||||
|
conn = tls.Client(conn, c.config.TLSConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the actual RPC client
|
||||||
|
c.client, err = NewRPCClient(conn, c.config.Plugins)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the stream syncing so that stdin, out, err work properly
|
||||||
|
err = c.client.SyncStreams(
|
||||||
|
c.config.SyncStdout,
|
||||||
|
c.config.SyncStderr)
|
||||||
|
if err != nil {
|
||||||
|
c.client.Close()
|
||||||
|
c.client = nil
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells whether or not the underlying process has exited.
|
||||||
|
func (c *Client) Exited() bool {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
return c.exited
|
||||||
|
}
|
||||||
|
|
||||||
|
// End the executing subprocess (if it is running) and perform any cleanup
|
||||||
|
// tasks necessary such as capturing any remaining logs and so on.
|
||||||
|
//
|
||||||
|
// This method blocks until the process successfully exits.
|
||||||
|
//
|
||||||
|
// This method can safely be called multiple times.
|
||||||
|
func (c *Client) Kill() {
|
||||||
|
// Grab a lock to read some private fields.
|
||||||
|
c.l.Lock()
|
||||||
|
process := c.process
|
||||||
|
addr := c.address
|
||||||
|
doneCh := c.doneLogging
|
||||||
|
c.l.Unlock()
|
||||||
|
|
||||||
|
// If there is no process, we never started anything. Nothing to kill.
|
||||||
|
if process == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check for address here. It is possible that the plugin
|
||||||
|
// started (process != nil) but has no address (addr == nil) if the
|
||||||
|
// plugin failed at startup. If we do have an address, we need to close
|
||||||
|
// the plugin net connections.
|
||||||
|
graceful := false
|
||||||
|
if addr != nil {
|
||||||
|
// Close the client to cleanly exit the process.
|
||||||
|
client, err := c.Client()
|
||||||
|
if err == nil {
|
||||||
|
err = client.Close()
|
||||||
|
|
||||||
|
// If there is no error, then we attempt to wait for a graceful
|
||||||
|
// exit. If there was an error, we assume that graceful cleanup
|
||||||
|
// won't happen and just force kill.
|
||||||
|
graceful = err == nil
|
||||||
|
if err != nil {
|
||||||
|
// If there was an error just log it. We're going to force
|
||||||
|
// kill in a moment anyways.
|
||||||
|
log.Printf(
|
||||||
|
"[WARN] plugin: error closing client during Kill: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're attempting a graceful exit, then we wait for a short period
|
||||||
|
// of time to allow that to happen. To wait for this we just wait on the
|
||||||
|
// doneCh which would be closed if the process exits.
|
||||||
|
if graceful {
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
return
|
||||||
|
case <-time.After(250 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If graceful exiting failed, just kill it
|
||||||
|
process.Kill()
|
||||||
|
|
||||||
|
// Wait for the client to finish logging so we have a complete log
|
||||||
|
<-doneCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the underlying subprocess, communicating with it to negotiate
|
||||||
|
// a port for RPC connections, and returning the address to connect via RPC.
|
||||||
|
//
|
||||||
|
// This method is safe to call multiple times. Subsequent calls have no effect.
|
||||||
|
// Once a client has been started once, it cannot be started again, even if
|
||||||
|
// it was killed.
|
||||||
|
func (c *Client) Start() (addr net.Addr, err error) {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
if c.address != nil {
|
||||||
|
return c.address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If one of cmd or reattach isn't set, then it is an error. We wrap
|
||||||
|
// this in a {} for scoping reasons, and hopeful that the escape
|
||||||
|
// analysis will pop the stock here.
|
||||||
|
{
|
||||||
|
cmdSet := c.config.Cmd != nil
|
||||||
|
attachSet := c.config.Reattach != nil
|
||||||
|
secureSet := c.config.SecureConfig != nil
|
||||||
|
if cmdSet == attachSet {
|
||||||
|
return nil, fmt.Errorf("Only one of Cmd or Reattach must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secureSet && attachSet {
|
||||||
|
return nil, ErrSecureConfigAndReattach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the logging channel for when we kill
|
||||||
|
c.doneLogging = make(chan struct{})
|
||||||
|
|
||||||
|
if c.config.Reattach != nil {
|
||||||
|
// Verify the process still exists. If not, then it is an error
|
||||||
|
p, err := os.FindProcess(c.config.Reattach.Pid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to connect to the addr since on Unix systems FindProcess
|
||||||
|
// doesn't actually return an error if it can't find the process.
|
||||||
|
conn, err := net.Dial(
|
||||||
|
c.config.Reattach.Addr.Network(),
|
||||||
|
c.config.Reattach.Addr.String())
|
||||||
|
if err != nil {
|
||||||
|
p.Kill()
|
||||||
|
return nil, ErrProcessNotFound
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
// Goroutine to mark exit status
|
||||||
|
go func(pid int) {
|
||||||
|
// Wait for the process to die
|
||||||
|
pidWait(pid)
|
||||||
|
|
||||||
|
// Log so we can see it
|
||||||
|
log.Printf("[DEBUG] plugin: reattached plugin process exited\n")
|
||||||
|
|
||||||
|
// Mark it
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
c.exited = true
|
||||||
|
|
||||||
|
// Close the logging channel since that doesn't work on reattach
|
||||||
|
close(c.doneLogging)
|
||||||
|
}(p.Pid)
|
||||||
|
|
||||||
|
// Set the address and process
|
||||||
|
c.address = c.config.Reattach.Addr
|
||||||
|
c.process = p
|
||||||
|
|
||||||
|
return c.address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
env := []string{
|
||||||
|
fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue),
|
||||||
|
fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort),
|
||||||
|
fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort),
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout_r, stdout_w := io.Pipe()
|
||||||
|
stderr_r, stderr_w := io.Pipe()
|
||||||
|
|
||||||
|
cmd := c.config.Cmd
|
||||||
|
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||||
|
cmd.Env = append(cmd.Env, env...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stderr = stderr_w
|
||||||
|
cmd.Stdout = stdout_w
|
||||||
|
|
||||||
|
if c.config.SecureConfig != nil {
|
||||||
|
if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil {
|
||||||
|
return nil, fmt.Errorf("error verifying checksum: %s", err)
|
||||||
|
} else if !ok {
|
||||||
|
return nil, ErrChecksumsDoNotMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] plugin: starting plugin: %s %#v", cmd.Path, cmd.Args)
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the process
|
||||||
|
c.process = cmd.Process
|
||||||
|
|
||||||
|
// Make sure the command is properly cleaned up if there is an error
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
|
||||||
|
if err != nil || r != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r != nil {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start goroutine to wait for process to exit
|
||||||
|
exitCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
// Make sure we close the write end of our stderr/stdout so
|
||||||
|
// that the readers send EOF properly.
|
||||||
|
defer stderr_w.Close()
|
||||||
|
defer stdout_w.Close()
|
||||||
|
|
||||||
|
// Wait for the command to end.
|
||||||
|
cmd.Wait()
|
||||||
|
|
||||||
|
// Log and make sure to flush the logs write away
|
||||||
|
log.Printf("[DEBUG] plugin: %s: plugin process exited\n", cmd.Path)
|
||||||
|
os.Stderr.Sync()
|
||||||
|
|
||||||
|
// Mark that we exited
|
||||||
|
close(exitCh)
|
||||||
|
|
||||||
|
// Set that we exited, which takes a lock
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
c.exited = true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start goroutine that logs the stderr
|
||||||
|
go c.logStderr(stderr_r)
|
||||||
|
|
||||||
|
// Start a goroutine that is going to be reading the lines
|
||||||
|
// out of stdout
|
||||||
|
linesCh := make(chan []byte)
|
||||||
|
go func() {
|
||||||
|
defer close(linesCh)
|
||||||
|
|
||||||
|
buf := bufio.NewReader(stdout_r)
|
||||||
|
for {
|
||||||
|
line, err := buf.ReadBytes('\n')
|
||||||
|
if line != nil {
|
||||||
|
linesCh <- line
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Make sure after we exit we read the lines from stdout forever
|
||||||
|
// so they don't block since it is an io.Pipe
|
||||||
|
defer func() {
|
||||||
|
go func() {
|
||||||
|
for _ = range linesCh {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Some channels for the next step
|
||||||
|
timeout := time.After(c.config.StartTimeout)
|
||||||
|
|
||||||
|
// Start looking for the address
|
||||||
|
log.Printf("[DEBUG] plugin: waiting for RPC address for: %s", cmd.Path)
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
err = errors.New("timeout while waiting for plugin to start")
|
||||||
|
case <-exitCh:
|
||||||
|
err = errors.New("plugin exited before we could connect")
|
||||||
|
case lineBytes := <-linesCh:
|
||||||
|
// Trim the line and split by "|" in order to get the parts of
|
||||||
|
// the output.
|
||||||
|
line := strings.TrimSpace(string(lineBytes))
|
||||||
|
parts := strings.SplitN(line, "|", 4)
|
||||||
|
if len(parts) < 4 {
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"Unrecognized remote plugin message: %s\n\n"+
|
||||||
|
"This usually means that the plugin is either invalid or simply\n"+
|
||||||
|
"needs to be recompiled to support the latest protocol.", line)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the core protocol. Wrapped in a {} for scoping.
|
||||||
|
{
|
||||||
|
var coreProtocol int64
|
||||||
|
coreProtocol, err = strconv.ParseInt(parts[0], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error parsing core protocol version: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(coreProtocol) != CoreProtocolVersion {
|
||||||
|
err = fmt.Errorf("Incompatible core API version with plugin. "+
|
||||||
|
"Plugin version: %s, Ours: %d\n\n"+
|
||||||
|
"To fix this, the plugin usually only needs to be recompiled.\n"+
|
||||||
|
"Please report this to the plugin author.", parts[0], CoreProtocolVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the protocol version
|
||||||
|
var protocol int64
|
||||||
|
protocol, err = strconv.ParseInt(parts[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error parsing protocol version: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the API version
|
||||||
|
if uint(protocol) != c.config.ProtocolVersion {
|
||||||
|
err = fmt.Errorf("Incompatible API version with plugin. "+
|
||||||
|
"Plugin version: %s, Ours: %d", parts[1], c.config.ProtocolVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[2] {
|
||||||
|
case "tcp":
|
||||||
|
addr, err = net.ResolveTCPAddr("tcp", parts[3])
|
||||||
|
case "unix":
|
||||||
|
addr, err = net.ResolveUnixAddr("unix", parts[3])
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Unknown address type: %s", parts[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.address = addr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReattachConfig returns the information that must be provided to NewClient
|
||||||
|
// to reattach to the plugin process that this client started. This is
|
||||||
|
// useful for plugins that detach from their parent process.
|
||||||
|
//
|
||||||
|
// If this returns nil then the process hasn't been started yet. Please
|
||||||
|
// call Start or Client before calling this.
|
||||||
|
func (c *Client) ReattachConfig() *ReattachConfig {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
if c.address == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.Cmd != nil && c.config.Cmd.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we connected via reattach, just return the information as-is
|
||||||
|
if c.config.Reattach != nil {
|
||||||
|
return c.config.Reattach
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReattachConfig{
|
||||||
|
Addr: c.address,
|
||||||
|
Pid: c.config.Cmd.Process.Pid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) logStderr(r io.Reader) {
|
||||||
|
bufR := bufio.NewReader(r)
|
||||||
|
for {
|
||||||
|
line, err := bufR.ReadString('\n')
|
||||||
|
if line != "" {
|
||||||
|
c.config.Stderr.Write([]byte(line))
|
||||||
|
|
||||||
|
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||||
|
log.Printf("[DEBUG] plugin: %s: %s", filepath.Base(c.config.Cmd.Path), line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag that we've completed logging for others
|
||||||
|
close(c.doneLogging)
|
||||||
|
}
|
28
vendor/github.com/hashicorp/go-plugin/discover.go
generated
vendored
Normal file
28
vendor/github.com/hashicorp/go-plugin/discover.go
generated
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Discover discovers plugins that are in a given directory.
|
||||||
|
//
|
||||||
|
// The directory doesn't need to be absolute. For example, "." will work fine.
|
||||||
|
//
|
||||||
|
// This currently assumes any file matching the glob is a plugin.
|
||||||
|
// In the future this may be smarter about checking that a file is
|
||||||
|
// executable and so on.
|
||||||
|
//
|
||||||
|
// TODO: test
|
||||||
|
func Discover(glob, dir string) ([]string, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Make the directory absolute if it isn't already
|
||||||
|
if !filepath.IsAbs(dir) {
|
||||||
|
dir, err = filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Glob(filepath.Join(dir, glob))
|
||||||
|
}
|
24
vendor/github.com/hashicorp/go-plugin/error.go
generated
vendored
Normal file
24
vendor/github.com/hashicorp/go-plugin/error.go
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
// This is a type that wraps error types so that they can be messaged
|
||||||
|
// across RPC channels. Since "error" is an interface, we can't always
|
||||||
|
// gob-encode the underlying structure. This is a valid error interface
|
||||||
|
// implementer that we will push across.
|
||||||
|
type BasicError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBasicError is used to create a BasicError.
|
||||||
|
//
|
||||||
|
// err is allowed to be nil.
|
||||||
|
func NewBasicError(err error) *BasicError {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BasicError{err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BasicError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
204
vendor/github.com/hashicorp/go-plugin/mux_broker.go
generated
vendored
Normal file
204
vendor/github.com/hashicorp/go-plugin/mux_broker.go
generated
vendored
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MuxBroker is responsible for brokering multiplexed connections by unique ID.
|
||||||
|
//
|
||||||
|
// It is used by plugins to multiplex multiple RPC connections and data
|
||||||
|
// streams on top of a single connection between the plugin process and the
|
||||||
|
// host process.
|
||||||
|
//
|
||||||
|
// This allows a plugin to request a channel with a specific ID to connect to
|
||||||
|
// or accept a connection from, and the broker handles the details of
|
||||||
|
// holding these channels open while they're being negotiated.
|
||||||
|
//
|
||||||
|
// The Plugin interface has access to these for both Server and Client.
|
||||||
|
// The broker can be used by either (optionally) to reserve and connect to
|
||||||
|
// new multiplexed streams. This is useful for complex args and return values,
|
||||||
|
// or anything else you might need a data stream for.
|
||||||
|
type MuxBroker struct {
|
||||||
|
nextId uint32
|
||||||
|
session *yamux.Session
|
||||||
|
streams map[uint32]*muxBrokerPending
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type muxBrokerPending struct {
|
||||||
|
ch chan net.Conn
|
||||||
|
doneCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMuxBroker(s *yamux.Session) *MuxBroker {
|
||||||
|
return &MuxBroker{
|
||||||
|
session: s,
|
||||||
|
streams: make(map[uint32]*muxBrokerPending),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept accepts a connection by ID.
|
||||||
|
//
|
||||||
|
// This should not be called multiple times with the same ID at one time.
|
||||||
|
func (m *MuxBroker) Accept(id uint32) (net.Conn, error) {
|
||||||
|
var c net.Conn
|
||||||
|
p := m.getStream(id)
|
||||||
|
select {
|
||||||
|
case c = <-p.ch:
|
||||||
|
close(p.doneCh)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
delete(m.streams, id)
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("timeout waiting for accept")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ack our connection
|
||||||
|
if err := binary.Write(c, binary.LittleEndian, id); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptAndServe is used to accept a specific stream ID and immediately
|
||||||
|
// serve an RPC server on that stream ID. This is used to easily serve
|
||||||
|
// complex arguments.
|
||||||
|
//
|
||||||
|
// The served interface is always registered to the "Plugin" name.
|
||||||
|
func (m *MuxBroker) AcceptAndServe(id uint32, v interface{}) {
|
||||||
|
conn, err := m.Accept(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] plugin: plugin acceptAndServe error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(conn, "Plugin", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection and all sub-connections.
|
||||||
|
func (m *MuxBroker) Close() error {
|
||||||
|
return m.session.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial opens a connection by ID.
|
||||||
|
func (m *MuxBroker) Dial(id uint32) (net.Conn, error) {
|
||||||
|
// Open the stream
|
||||||
|
stream, err := m.session.OpenStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the stream ID onto the wire.
|
||||||
|
if err := binary.Write(stream, binary.LittleEndian, id); err != nil {
|
||||||
|
stream.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the ack that we connected. Then we're off!
|
||||||
|
var ack uint32
|
||||||
|
if err := binary.Read(stream, binary.LittleEndian, &ack); err != nil {
|
||||||
|
stream.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ack != id {
|
||||||
|
stream.Close()
|
||||||
|
return nil, fmt.Errorf("bad ack: %d (expected %d)", ack, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextId returns a unique ID to use next.
|
||||||
|
//
|
||||||
|
// It is possible for very long-running plugin hosts to wrap this value,
|
||||||
|
// though it would require a very large amount of RPC calls. In practice
|
||||||
|
// we've never seen it happen.
|
||||||
|
func (m *MuxBroker) NextId() uint32 {
|
||||||
|
return atomic.AddUint32(&m.nextId, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the brokering and should be executed in a goroutine, since it
|
||||||
|
// blocks forever, or until the session closes.
|
||||||
|
//
|
||||||
|
// Uses of MuxBroker never need to call this. It is called internally by
|
||||||
|
// the plugin host/client.
|
||||||
|
func (m *MuxBroker) Run() {
|
||||||
|
for {
|
||||||
|
stream, err := m.session.AcceptStream()
|
||||||
|
if err != nil {
|
||||||
|
// Once we receive an error, just exit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the stream ID from the stream
|
||||||
|
var id uint32
|
||||||
|
if err := binary.Read(stream, binary.LittleEndian, &id); err != nil {
|
||||||
|
stream.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the waiter
|
||||||
|
p := m.getStream(id)
|
||||||
|
select {
|
||||||
|
case p.ch <- stream:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a timeout
|
||||||
|
go m.timeoutWait(id, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MuxBroker) getStream(id uint32) *muxBrokerPending {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
p, ok := m.streams[id]
|
||||||
|
if ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
m.streams[id] = &muxBrokerPending{
|
||||||
|
ch: make(chan net.Conn, 1),
|
||||||
|
doneCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
return m.streams[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MuxBroker) timeoutWait(id uint32, p *muxBrokerPending) {
|
||||||
|
// Wait for the stream to either be picked up and connected, or
|
||||||
|
// for a timeout.
|
||||||
|
timeout := false
|
||||||
|
select {
|
||||||
|
case <-p.doneCh:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
timeout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
// Delete the stream so no one else can grab it
|
||||||
|
delete(m.streams, id)
|
||||||
|
|
||||||
|
// If we timed out, then check if we have a channel in the buffer,
|
||||||
|
// and if so, close it.
|
||||||
|
if timeout {
|
||||||
|
select {
|
||||||
|
case s := <-p.ch:
|
||||||
|
s.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
vendor/github.com/hashicorp/go-plugin/plugin.go
generated
vendored
Normal file
25
vendor/github.com/hashicorp/go-plugin/plugin.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// The plugin package exposes functions and helpers for communicating to
|
||||||
|
// plugins which are implemented as standalone binary applications.
|
||||||
|
//
|
||||||
|
// plugin.Client fully manages the lifecycle of executing the application,
|
||||||
|
// connecting to it, and returning the RPC client for dispensing plugins.
|
||||||
|
//
|
||||||
|
// plugin.Serve fully manages listeners to expose an RPC server from a binary
|
||||||
|
// that plugin.Client can connect to.
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin is the interface that is implemented to serve/connect to an
|
||||||
|
// inteface implementation.
|
||||||
|
type Plugin interface {
|
||||||
|
// Server should return the RPC server compatible struct to serve
|
||||||
|
// the methods that the Client calls over net/rpc.
|
||||||
|
Server(*MuxBroker) (interface{}, error)
|
||||||
|
|
||||||
|
// Client returns an interface implementation for the plugin you're
|
||||||
|
// serving that communicates to the server end of the plugin.
|
||||||
|
Client(*MuxBroker, *rpc.Client) (interface{}, error)
|
||||||
|
}
|
24
vendor/github.com/hashicorp/go-plugin/process.go
generated
vendored
Normal file
24
vendor/github.com/hashicorp/go-plugin/process.go
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pidAlive checks whether a pid is alive.
|
||||||
|
func pidAlive(pid int) bool {
|
||||||
|
return _pidAlive(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pidWait blocks for a process to exit.
|
||||||
|
func pidWait(pid int) error {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if !pidAlive(pid) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
19
vendor/github.com/hashicorp/go-plugin/process_posix.go
generated
vendored
Normal file
19
vendor/github.com/hashicorp/go-plugin/process_posix.go
generated
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// _pidAlive tests whether a process is alive or not by sending it Signal 0,
|
||||||
|
// since Go otherwise has no way to test this.
|
||||||
|
func _pidAlive(pid int) bool {
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err == nil {
|
||||||
|
err = proc.Signal(syscall.Signal(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
29
vendor/github.com/hashicorp/go-plugin/process_windows.go
generated
vendored
Normal file
29
vendor/github.com/hashicorp/go-plugin/process_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Weird name but matches the MSDN docs
|
||||||
|
exit_STILL_ACTIVE = 259
|
||||||
|
|
||||||
|
processDesiredAccess = syscall.STANDARD_RIGHTS_READ |
|
||||||
|
syscall.PROCESS_QUERY_INFORMATION |
|
||||||
|
syscall.SYNCHRONIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
// _pidAlive tests whether a process is alive or not
|
||||||
|
func _pidAlive(pid int) bool {
|
||||||
|
h, err := syscall.OpenProcess(processDesiredAccess, false, uint32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var ec uint32
|
||||||
|
if e := syscall.GetExitCodeProcess(h, &ec); e != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ec == exit_STILL_ACTIVE
|
||||||
|
}
|
123
vendor/github.com/hashicorp/go-plugin/rpc_client.go
generated
vendored
Normal file
123
vendor/github.com/hashicorp/go-plugin/rpc_client.go
generated
vendored
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/rpc"
|
||||||
|
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPCClient connects to an RPCServer over net/rpc to dispense plugin types.
|
||||||
|
type RPCClient struct {
|
||||||
|
broker *MuxBroker
|
||||||
|
control *rpc.Client
|
||||||
|
plugins map[string]Plugin
|
||||||
|
|
||||||
|
// These are the streams used for the various stdout/err overrides
|
||||||
|
stdout, stderr net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRPCClient creates a client from an already-open connection-like value.
|
||||||
|
// Dial is typically used instead.
|
||||||
|
func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClient, error) {
|
||||||
|
// Create the yamux client so we can multiplex
|
||||||
|
mux, err := yamux.Client(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the control stream.
|
||||||
|
control, err := mux.Open()
|
||||||
|
if err != nil {
|
||||||
|
mux.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect stdout, stderr streams
|
||||||
|
stdstream := make([]net.Conn, 2)
|
||||||
|
for i, _ := range stdstream {
|
||||||
|
stdstream[i], err = mux.Open()
|
||||||
|
if err != nil {
|
||||||
|
mux.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the broker and start it up
|
||||||
|
broker := newMuxBroker(mux)
|
||||||
|
go broker.Run()
|
||||||
|
|
||||||
|
// Build the client using our broker and control channel.
|
||||||
|
return &RPCClient{
|
||||||
|
broker: broker,
|
||||||
|
control: rpc.NewClient(control),
|
||||||
|
plugins: plugins,
|
||||||
|
stdout: stdstream[0],
|
||||||
|
stderr: stdstream[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncStreams should be called to enable syncing of stdout,
|
||||||
|
// stderr with the plugin.
|
||||||
|
//
|
||||||
|
// This will return immediately and the syncing will continue to happen
|
||||||
|
// in the background. You do not need to launch this in a goroutine itself.
|
||||||
|
//
|
||||||
|
// This should never be called multiple times.
|
||||||
|
func (c *RPCClient) SyncStreams(stdout io.Writer, stderr io.Writer) error {
|
||||||
|
go copyStream("stdout", stdout, c.stdout)
|
||||||
|
go copyStream("stderr", stderr, c.stderr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection. The client is no longer usable after this
|
||||||
|
// is called.
|
||||||
|
func (c *RPCClient) Close() error {
|
||||||
|
// Call the control channel and ask it to gracefully exit. If this
|
||||||
|
// errors, then we save it so that we always return an error but we
|
||||||
|
// want to try to close the other channels anyways.
|
||||||
|
var empty struct{}
|
||||||
|
returnErr := c.control.Call("Control.Quit", true, &empty)
|
||||||
|
|
||||||
|
// Close the other streams we have
|
||||||
|
if err := c.control.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.stdout.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.stderr.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.broker.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return back the error we got from Control.Quit. This is very important
|
||||||
|
// since we MUST return non-nil error if this fails so that Client.Kill
|
||||||
|
// will properly try a process.Kill.
|
||||||
|
return returnErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RPCClient) Dispense(name string) (interface{}, error) {
|
||||||
|
p, ok := c.plugins[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown plugin type: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id uint32
|
||||||
|
if err := c.control.Call(
|
||||||
|
"Dispenser.Dispense", name, &id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := c.broker.Dial(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Client(c.broker, rpc.NewClient(conn))
|
||||||
|
}
|
185
vendor/github.com/hashicorp/go-plugin/rpc_server.go
generated
vendored
Normal file
185
vendor/github.com/hashicorp/go-plugin/rpc_server.go
generated
vendored
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/rpc"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPCServer listens for network connections and then dispenses interface
|
||||||
|
// implementations over net/rpc.
|
||||||
|
//
|
||||||
|
// After setting the fields below, they shouldn't be read again directly
|
||||||
|
// from the structure which may be reading/writing them concurrently.
|
||||||
|
type RPCServer struct {
|
||||||
|
Plugins map[string]Plugin
|
||||||
|
|
||||||
|
// Stdout, Stderr are what this server will use instead of the
|
||||||
|
// normal stdin/out/err. This is because due to the multi-process nature
|
||||||
|
// of our plugin system, we can't use the normal process values so we
|
||||||
|
// make our own custom one we pipe across.
|
||||||
|
Stdout io.Reader
|
||||||
|
Stderr io.Reader
|
||||||
|
|
||||||
|
// DoneCh should be set to a non-nil channel that will be closed
|
||||||
|
// when the control requests the RPC server to end.
|
||||||
|
DoneCh chan<- struct{}
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept accepts connections on a listener and serves requests for
|
||||||
|
// each incoming connection. Accept blocks; the caller typically invokes
|
||||||
|
// it in a go statement.
|
||||||
|
func (s *RPCServer) Accept(lis net.Listener) {
|
||||||
|
for {
|
||||||
|
conn, err := lis.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] plugin: plugin server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.ServeConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeConn runs a single connection.
|
||||||
|
//
|
||||||
|
// ServeConn blocks, serving the connection until the client hangs up.
|
||||||
|
func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) {
|
||||||
|
// First create the yamux server to wrap this connection
|
||||||
|
mux, err := yamux.Server(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
log.Printf("[ERR] plugin: error creating yamux server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept the control connection
|
||||||
|
control, err := mux.Accept()
|
||||||
|
if err != nil {
|
||||||
|
mux.Close()
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Printf("[ERR] plugin: error accepting control connection: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the stdstreams (in, out, err)
|
||||||
|
stdstream := make([]net.Conn, 2)
|
||||||
|
for i, _ := range stdstream {
|
||||||
|
stdstream[i], err = mux.Accept()
|
||||||
|
if err != nil {
|
||||||
|
mux.Close()
|
||||||
|
log.Printf("[ERR] plugin: accepting stream %d: %s", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy std streams out to the proper place
|
||||||
|
go copyStream("stdout", stdstream[0], s.Stdout)
|
||||||
|
go copyStream("stderr", stdstream[1], s.Stderr)
|
||||||
|
|
||||||
|
// Create the broker and start it up
|
||||||
|
broker := newMuxBroker(mux)
|
||||||
|
go broker.Run()
|
||||||
|
|
||||||
|
// Use the control connection to build the dispenser and serve the
|
||||||
|
// connection.
|
||||||
|
server := rpc.NewServer()
|
||||||
|
server.RegisterName("Control", &controlServer{
|
||||||
|
server: s,
|
||||||
|
})
|
||||||
|
server.RegisterName("Dispenser", &dispenseServer{
|
||||||
|
broker: broker,
|
||||||
|
plugins: s.Plugins,
|
||||||
|
})
|
||||||
|
server.ServeConn(control)
|
||||||
|
}
|
||||||
|
|
||||||
|
// done is called internally by the control server to trigger the
|
||||||
|
// doneCh to close which is listened to by the main process to cleanly
|
||||||
|
// exit.
|
||||||
|
func (s *RPCServer) done() {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
if s.DoneCh != nil {
|
||||||
|
close(s.DoneCh)
|
||||||
|
s.DoneCh = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispenseServer dispenses variousinterface implementations for Terraform.
|
||||||
|
type controlServer struct {
|
||||||
|
server *RPCServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controlServer) Quit(
|
||||||
|
null bool, response *struct{}) error {
|
||||||
|
// End the server
|
||||||
|
c.server.done()
|
||||||
|
|
||||||
|
// Always return true
|
||||||
|
*response = struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispenseServer dispenses variousinterface implementations for Terraform.
|
||||||
|
type dispenseServer struct {
|
||||||
|
broker *MuxBroker
|
||||||
|
plugins map[string]Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dispenseServer) Dispense(
|
||||||
|
name string, response *uint32) error {
|
||||||
|
// Find the function to create this implementation
|
||||||
|
p, ok := d.plugins[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown plugin type: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the implementation first so we know if there is an error.
|
||||||
|
impl, err := p.Server(d.broker)
|
||||||
|
if err != nil {
|
||||||
|
// We turn the error into an errors error so that it works across RPC
|
||||||
|
return errors.New(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve an ID for our implementation
|
||||||
|
id := d.broker.NextId()
|
||||||
|
*response = id
|
||||||
|
|
||||||
|
// Run the rest in a goroutine since it can only happen once this RPC
|
||||||
|
// call returns. We wait for a connection for the plugin implementation
|
||||||
|
// and serve it.
|
||||||
|
go func() {
|
||||||
|
conn, err := d.broker.Accept(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] go-plugin: plugin dispense error: %s: %s", name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(conn, "Plugin", impl)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve(conn io.ReadWriteCloser, name string, v interface{}) {
|
||||||
|
server := rpc.NewServer()
|
||||||
|
if err := server.RegisterName(name, v); err != nil {
|
||||||
|
log.Printf("[ERR] go-plugin: plugin dispense error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server.ServeConn(conn)
|
||||||
|
}
|
235
vendor/github.com/hashicorp/go-plugin/server.go
generated
vendored
Normal file
235
vendor/github.com/hashicorp/go-plugin/server.go
generated
vendored
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoreProtocolVersion is the ProtocolVersion of the plugin system itself.
|
||||||
|
// We will increment this whenever we change any protocol behavior. This
|
||||||
|
// will invalidate any prior plugins but will at least allow us to iterate
|
||||||
|
// on the core in a safe way. We will do our best to do this very
|
||||||
|
// infrequently.
|
||||||
|
const CoreProtocolVersion = 1
|
||||||
|
|
||||||
|
// HandshakeConfig is the configuration used by client and servers to
|
||||||
|
// handshake before starting a plugin connection. This is embedded by
|
||||||
|
// both ServeConfig and ClientConfig.
|
||||||
|
//
|
||||||
|
// In practice, the plugin host creates a HandshakeConfig that is exported
|
||||||
|
// and plugins then can easily consume it.
|
||||||
|
type HandshakeConfig struct {
|
||||||
|
// ProtocolVersion is the version that clients must match on to
|
||||||
|
// agree they can communicate. This should match the ProtocolVersion
|
||||||
|
// set on ClientConfig when using a plugin.
|
||||||
|
ProtocolVersion uint
|
||||||
|
|
||||||
|
// MagicCookieKey and value are used as a very basic verification
|
||||||
|
// that a plugin is intended to be launched. This is not a security
|
||||||
|
// measure, just a UX feature. If the magic cookie doesn't match,
|
||||||
|
// we show human-friendly output.
|
||||||
|
MagicCookieKey string
|
||||||
|
MagicCookieValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeConfig configures what sorts of plugins are served.
|
||||||
|
type ServeConfig struct {
|
||||||
|
// HandshakeConfig is the configuration that must match clients.
|
||||||
|
HandshakeConfig
|
||||||
|
|
||||||
|
// Plugins are the plugins that are served.
|
||||||
|
Plugins map[string]Plugin
|
||||||
|
|
||||||
|
// TLSProvider is a function that returns a configured tls.Config.
|
||||||
|
TLSProvider func() (*tls.Config, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve serves the plugins given by ServeConfig.
|
||||||
|
//
|
||||||
|
// Serve doesn't return until the plugin is done being executed. Any
|
||||||
|
// errors will be outputted to the log.
|
||||||
|
//
|
||||||
|
// This is the method that plugins should call in their main() functions.
|
||||||
|
func Serve(opts *ServeConfig) {
|
||||||
|
// Validate the handshake config
|
||||||
|
if opts.MagicCookieKey == "" || opts.MagicCookieValue == "" {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"Misconfigured ServeConfig given to serve this plugin: no magic cookie\n"+
|
||||||
|
"key or value was set. Please notify the plugin author and report\n"+
|
||||||
|
"this as a bug.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check the cookie
|
||||||
|
if os.Getenv(opts.MagicCookieKey) != opts.MagicCookieValue {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"This binary is a plugin. These are not meant to be executed directly.\n"+
|
||||||
|
"Please execute the program that consumes these plugins, which will\n"+
|
||||||
|
"load any plugins automatically\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging goes to the original stderr
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
// Create our new stdout, stderr files. These will override our built-in
|
||||||
|
// stdout/stderr so that it works across the stream boundary.
|
||||||
|
stdout_r, stdout_w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
stderr_r, stderr_w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a listener so we can accept a connection
|
||||||
|
listener, err := serverListener()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] plugin: plugin init: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.TLSProvider != nil {
|
||||||
|
tlsConfig, err := opts.TLSProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] plugin: plugin tls init: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listener = tls.NewListener(listener, tlsConfig)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Create the channel to tell us when we're done
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
// Create the RPC server to dispense
|
||||||
|
server := &RPCServer{
|
||||||
|
Plugins: opts.Plugins,
|
||||||
|
Stdout: stdout_r,
|
||||||
|
Stderr: stderr_r,
|
||||||
|
DoneCh: doneCh,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the address and service name to stdout so that core can bring it up.
|
||||||
|
log.Printf("[DEBUG] plugin: plugin address: %s %s\n",
|
||||||
|
listener.Addr().Network(), listener.Addr().String())
|
||||||
|
fmt.Printf("%d|%d|%s|%s\n",
|
||||||
|
CoreProtocolVersion,
|
||||||
|
opts.ProtocolVersion,
|
||||||
|
listener.Addr().Network(),
|
||||||
|
listener.Addr().String())
|
||||||
|
os.Stdout.Sync()
|
||||||
|
|
||||||
|
// Eat the interrupts
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
var count int32 = 0
|
||||||
|
for {
|
||||||
|
<-ch
|
||||||
|
newCount := atomic.AddInt32(&count, 1)
|
||||||
|
log.Printf(
|
||||||
|
"[DEBUG] plugin: received interrupt signal (count: %d). Ignoring.",
|
||||||
|
newCount)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set our new out, err
|
||||||
|
os.Stdout = stdout_w
|
||||||
|
os.Stderr = stderr_w
|
||||||
|
|
||||||
|
// Serve
|
||||||
|
go server.Accept(listener)
|
||||||
|
|
||||||
|
// Wait for the graceful exit
|
||||||
|
<-doneCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverListener() (net.Listener, error) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return serverListener_tcp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverListener_unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverListener_tcp() (net.Listener, error) {
|
||||||
|
minPort, err := strconv.ParseInt(os.Getenv("PLUGIN_MIN_PORT"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPort, err := strconv.ParseInt(os.Getenv("PLUGIN_MAX_PORT"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for port := minPort; port <= maxPort; port++ {
|
||||||
|
address := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
listener, err := net.Listen("tcp", address)
|
||||||
|
if err == nil {
|
||||||
|
return listener, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Couldn't bind plugin TCP listener")
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverListener_unix() (net.Listener, error) {
|
||||||
|
tf, err := ioutil.TempFile("", "plugin")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path := tf.Name()
|
||||||
|
|
||||||
|
// Close the file and remove it because it has to not exist for
|
||||||
|
// the domain socket.
|
||||||
|
if err := tf.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.Listen("unix", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the listener in rmListener so that the Unix domain socket file
|
||||||
|
// is removed on close.
|
||||||
|
return &rmListener{
|
||||||
|
Listener: l,
|
||||||
|
Path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rmListener is an implementation of net.Listener that forwards most
|
||||||
|
// calls to the listener but also removes a file as part of the close. We
|
||||||
|
// use this to cleanup the unix domain socket on close.
|
||||||
|
type rmListener struct {
|
||||||
|
net.Listener
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *rmListener) Close() error {
|
||||||
|
// Close the listener itself
|
||||||
|
if err := l.Listener.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the file
|
||||||
|
return os.Remove(l.Path)
|
||||||
|
}
|
31
vendor/github.com/hashicorp/go-plugin/server_mux.go
generated
vendored
Normal file
31
vendor/github.com/hashicorp/go-plugin/server_mux.go
generated
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeMuxMap is the type that is used to configure ServeMux
|
||||||
|
type ServeMuxMap map[string]*ServeConfig
|
||||||
|
|
||||||
|
// ServeMux is like Serve, but serves multiple types of plugins determined
|
||||||
|
// by the argument given on the command-line.
|
||||||
|
//
|
||||||
|
// This command doesn't return until the plugin is done being executed. Any
|
||||||
|
// errors are logged or output to stderr.
|
||||||
|
func ServeMux(m ServeMuxMap) {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"Invoked improperly. This is an internal command that shouldn't\n"+
|
||||||
|
"be manually invoked.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, ok := m[os.Args[1]]
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown plugin: %s\n", os.Args[1])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Serve(opts)
|
||||||
|
}
|
18
vendor/github.com/hashicorp/go-plugin/stream.go
generated
vendored
Normal file
18
vendor/github.com/hashicorp/go-plugin/stream.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyStream(name string, dst io.Writer, src io.Reader) {
|
||||||
|
if src == nil {
|
||||||
|
panic(name + ": src is nil")
|
||||||
|
}
|
||||||
|
if dst == nil {
|
||||||
|
panic(name + ": dst is nil")
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(dst, src); err != nil && err != io.EOF {
|
||||||
|
log.Printf("[ERR] plugin: stream copy '%s' error: %s", name, err)
|
||||||
|
}
|
||||||
|
}
|
76
vendor/github.com/hashicorp/go-plugin/testing.go
generated
vendored
Normal file
76
vendor/github.com/hashicorp/go-plugin/testing.go
generated
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
"net/rpc"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The testing file contains test helpers that you can use outside of
|
||||||
|
// this package for making it easier to test plugins themselves.
|
||||||
|
|
||||||
|
// TestConn is a helper function for returning a client and server
|
||||||
|
// net.Conn connected to each other.
|
||||||
|
func TestConn(t *testing.T) (net.Conn, net.Conn) {
|
||||||
|
// Listen to any local port. This listener will be closed
|
||||||
|
// after a single connection is established.
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a goroutine to accept our client connection
|
||||||
|
var serverConn net.Conn
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(doneCh)
|
||||||
|
defer l.Close()
|
||||||
|
var err error
|
||||||
|
serverConn, err = l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
clientConn, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the server side to acknowledge it has connected
|
||||||
|
<-doneCh
|
||||||
|
|
||||||
|
return clientConn, serverConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRPCConn returns a rpc client and server connected to each other.
|
||||||
|
func TestRPCConn(t *testing.T) (*rpc.Client, *rpc.Server) {
|
||||||
|
clientConn, serverConn := TestConn(t)
|
||||||
|
|
||||||
|
server := rpc.NewServer()
|
||||||
|
go server.ServeConn(serverConn)
|
||||||
|
|
||||||
|
client := rpc.NewClient(clientConn)
|
||||||
|
return client, server
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPluginRPCConn returns a plugin RPC client and server that are connected
|
||||||
|
// together and configured.
|
||||||
|
func TestPluginRPCConn(t *testing.T, ps map[string]Plugin) (*RPCClient, *RPCServer) {
|
||||||
|
// Create two net.Conns we can use to shuttle our control connection
|
||||||
|
clientConn, serverConn := TestConn(t)
|
||||||
|
|
||||||
|
// Start up the server
|
||||||
|
server := &RPCServer{Plugins: ps, Stdout: new(bytes.Buffer), Stderr: new(bytes.Buffer)}
|
||||||
|
go server.ServeConn(serverConn)
|
||||||
|
|
||||||
|
// Connect the client to the server
|
||||||
|
client, err := NewRPCClient(clientConn, ps)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, server
|
||||||
|
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
@ -846,6 +846,12 @@
|
||||||
"revision": "ed905158d87462226a13fe39ddf685ea65f1c11f",
|
"revision": "ed905158d87462226a13fe39ddf685ea65f1c11f",
|
||||||
"revisionTime": "2016-12-16T18:43:04Z"
|
"revisionTime": "2016-12-16T18:43:04Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "FOLPOFo4xuUaErsL99EC8azEUjw=",
|
||||||
|
"path": "github.com/hashicorp/go-plugin",
|
||||||
|
"revision": "b6691c5cfe7f0ec984114b056889cc90e51e38d0",
|
||||||
|
"revisionTime": "2017-04-12T21:16:38Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "ErJHGU6AVPZM9yoY/xV11TwSjQs=",
|
"checksumSHA1": "ErJHGU6AVPZM9yoY/xV11TwSjQs=",
|
||||||
"path": "github.com/hashicorp/go-retryablehttp",
|
"path": "github.com/hashicorp/go-retryablehttp",
|
||||||
|
|
96
website/source/api/secret/databases/cassandra.html.md
Normal file
96
website/source/api/secret/databases/cassandra.html.md
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "Cassandra Database Plugin - HTTP API"
|
||||||
|
sidebar_current: "docs-http-secret-databases-cassandra-maria"
|
||||||
|
description: |-
|
||||||
|
The Cassandra plugin for Vault's Database backend generates database credentials to access Cassandra servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cassandra Database Plugin HTTP API
|
||||||
|
|
||||||
|
The Cassandra Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the Cassandra database.
|
||||||
|
|
||||||
|
## Configure Connection
|
||||||
|
|
||||||
|
In addition to the parameters defined by the [Database
|
||||||
|
Backend](/api/secret/databases/index.html#configure-connection), this plugin
|
||||||
|
has a number of parameters to further configure a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- `hosts` `(string: <required>)` – Specifies a set of comma-delineated Cassandra
|
||||||
|
hosts to connect to.
|
||||||
|
|
||||||
|
- `username` `(string: <required>)` – Specifies the username to use for
|
||||||
|
superuser access.
|
||||||
|
|
||||||
|
- `password` `(string: <required>)` – Specifies the password corresponding to
|
||||||
|
the given username.
|
||||||
|
|
||||||
|
- `tls` `(bool: true)` – Specifies whether to use TLS when connecting to
|
||||||
|
Cassandra.
|
||||||
|
|
||||||
|
- `insecure_tls` `(bool: false)` – Specifies whether to skip verification of the
|
||||||
|
server certificate when using TLS.
|
||||||
|
|
||||||
|
- `pem_bundle` `(string: "")` – Specifies concatenated PEM blocks containing a
|
||||||
|
certificate and private key; a certificate, private key, and issuing CA
|
||||||
|
certificate; or just a CA certificate.
|
||||||
|
|
||||||
|
- `pem_json` `(string: "")` – Specifies JSON containing a certificate and
|
||||||
|
private key; a certificate, private key, and issuing CA certificate; or just a
|
||||||
|
CA certificate. For convenience format is the same as the output of the
|
||||||
|
`issue` command from the `pki` backend; see
|
||||||
|
[the pki documentation](/docs/secrets/pki/index.html).
|
||||||
|
|
||||||
|
- `protocol_version` `(int: 2)` – Specifies the CQL protocol version to use.
|
||||||
|
|
||||||
|
- `connect_timeout` `(string: "5s")` – Specifies the connection timeout to use.
|
||||||
|
|
||||||
|
TLS works as follows:
|
||||||
|
|
||||||
|
- If `tls` is set to true, the connection will use TLS; this happens
|
||||||
|
automatically if `pem_bundle`, `pem_json`, or `insecure_tls` is set
|
||||||
|
|
||||||
|
- If `insecure_tls` is set to true, the connection will not perform verification
|
||||||
|
of the server certificate; this also sets `tls` to true
|
||||||
|
|
||||||
|
- If only `issuing_ca` is set in `pem_json`, or the only certificate in
|
||||||
|
`pem_bundle` is a CA certificate, the given CA certificate will be used for
|
||||||
|
server certificate verification; otherwise the system CA certificates will be
|
||||||
|
used
|
||||||
|
|
||||||
|
- If `certificate` and `private_key` are set in `pem_bundle` or `pem_json`,
|
||||||
|
client auth will be turned on for the connection
|
||||||
|
|
||||||
|
`pem_bundle` should be a PEM-concatenated bundle of a private key + client
|
||||||
|
certificate, an issuing CA certificate, or both. `pem_json` should contain the
|
||||||
|
same information; for convenience, the JSON format is the same as that output by
|
||||||
|
the issue command from the PKI backend.
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin_name": "cassandra-database-plugin",
|
||||||
|
"allowed_roles": "readonly",
|
||||||
|
"hosts": "cassandra1.local",
|
||||||
|
"username": "user",
|
||||||
|
"password": "pass"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/cassandra/config/connection
|
||||||
|
```
|
344
website/source/api/secret/databases/index.html.md
Normal file
344
website/source/api/secret/databases/index.html.md
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "Databases - HTTP API"
|
||||||
|
sidebar_current: "docs-http-secret-databases"
|
||||||
|
description: |-
|
||||||
|
Top page for database secret backend information
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Secret Backend HTTP API
|
||||||
|
|
||||||
|
This is the API documentation for the Vault Database secret backend. For
|
||||||
|
general information about the usage and operation of the Database backend,
|
||||||
|
please see the
|
||||||
|
[Vault Database backend documentation](/docs/secrets/databases/index.html).
|
||||||
|
|
||||||
|
This documentation assumes the Database backend is mounted at the
|
||||||
|
`/database` path in Vault. Since it is possible to mount secret backends at
|
||||||
|
any location, please update your API calls accordingly.
|
||||||
|
|
||||||
|
## Configure Connection
|
||||||
|
|
||||||
|
This endpoint configures the connection string used to communicate with the
|
||||||
|
desired database. In addition to the parameters listed here, each Database
|
||||||
|
plugin has additional, database plugin specifig, parameters for this endpoint.
|
||||||
|
Please read the HTTP API for the plugin you'd wish to configure to see the full
|
||||||
|
list of additional parameters.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- `name` `(string: <required>)` – Specifies the name for this database
|
||||||
|
connection. This is specified as part of the URL.
|
||||||
|
|
||||||
|
- `plugin_name` `(string: <required>)` - Specifies the name of the plugin to use
|
||||||
|
for this connection.
|
||||||
|
|
||||||
|
- `verify_connection` `(bool: true)` – Specifies if the connection is verified
|
||||||
|
during initial configuration. Defaults to true.
|
||||||
|
|
||||||
|
- `allowed_roles` `(slice: [])` - Array or comma separated string of the roles
|
||||||
|
allowed to use this connection. Defaults to empty (no roles), if contains a
|
||||||
|
"*" any role can use this connection.
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin_name": "mysql-database-plugin",
|
||||||
|
"allowed_roles": "readonly",
|
||||||
|
"connection_url": "root:mysql@tcp(127.0.0.1:3306)/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/database/config/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read Connection
|
||||||
|
|
||||||
|
This endpoint returns the configuration settings for a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `GET` | `/database/config/:name` | `200 application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the connection to read.
|
||||||
|
This is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request GET \
|
||||||
|
https://vault.rocks/v1/database/config/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"allowed_roles": [
|
||||||
|
"readonly"
|
||||||
|
],
|
||||||
|
"connection_details": {
|
||||||
|
"connection_url": "root:mysql@tcp(127.0.0.1:3306)/",
|
||||||
|
},
|
||||||
|
"plugin_name": "mysql-database-plugin"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete Connection
|
||||||
|
|
||||||
|
This endpoint deletes a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `DELETE` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the connection to delete.
|
||||||
|
This is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request DELETE \
|
||||||
|
https://vault.rocks/v1/database/config/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reset Connection
|
||||||
|
|
||||||
|
This endpoint closes a connection and it's underlying plugin and restarts it
|
||||||
|
with the configuration stored in the barrier.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/reset/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the connection to delete.
|
||||||
|
This is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
https://vault.rocks/v1/database/reset/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create Role
|
||||||
|
|
||||||
|
This endpoint creates or updates a role definition.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/roles/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the role to create. This
|
||||||
|
is specified as part of the URL.
|
||||||
|
|
||||||
|
- `db_name` `(string: <required>)` - The name of the database connection to use
|
||||||
|
for this role.
|
||||||
|
|
||||||
|
- `default_ttl` `(string/int: 0)` - Specifies the TTL for the leases
|
||||||
|
associated with this role. Accepts time suffixed strings ("1h") or an integer
|
||||||
|
number of seconds. Defaults to system/backend default TTL time.
|
||||||
|
|
||||||
|
- `max_ttl` `(string/int: 0)` - Specifies the maximum TTL for the leases
|
||||||
|
associated with this role. Accepts time suffixed strings ("1h") or an integer
|
||||||
|
number of seconds. Defaults to system/backend default TTL time.
|
||||||
|
|
||||||
|
- `creation_statements` `(string: <required>)` – Specifies the database
|
||||||
|
statements executed to create and configure a user. Must be a
|
||||||
|
semicolon-separated string, a base64-encoded semicolon-separated string, a
|
||||||
|
serialized JSON string array, or a base64-encoded serialized JSON string
|
||||||
|
array. The '{{name}}', '{{password}}' and '{{expiration}}' values will be
|
||||||
|
substituted.
|
||||||
|
|
||||||
|
- `revocation_statements` `(string: "")` – Specifies the database statements to
|
||||||
|
be executed to revoke a user. Must be a semicolon-separated string, a
|
||||||
|
base64-encoded semicolon-separated string, a serialized JSON string array, or
|
||||||
|
a base64-encoded serialized JSON string array. The '{{name}}' value will be
|
||||||
|
substituted.
|
||||||
|
|
||||||
|
- `rollback_statements` `(string: "")` – Specifies the database statements to be
|
||||||
|
executed rollback a create operation in the event of an error. Not every
|
||||||
|
plugin type will support this functionality. Must be a semicolon-separated
|
||||||
|
string, a base64-encoded semicolon-separated string, a serialized JSON string
|
||||||
|
array, or a base64-encoded serialized JSON string array. The '{{name}}' value
|
||||||
|
will be substituted.
|
||||||
|
|
||||||
|
- `renew_statements` `(string: "")` – Specifies the database statements to be
|
||||||
|
executed to renew a user. Not every plugin type will support this
|
||||||
|
functionality. Must be a semicolon-separated string, a base64-encoded
|
||||||
|
semicolon-separated string, a serialized JSON string array, or a
|
||||||
|
base64-encoded serialized JSON string array. The '{{name}}' and
|
||||||
|
'{{expiration}}` values will be substituted.
|
||||||
|
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"db_name": "mysql",
|
||||||
|
"creation_statements": "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';",
|
||||||
|
"default_ttl": "1h",
|
||||||
|
"max_ttl": "24h"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/database/roles/my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read Role
|
||||||
|
|
||||||
|
This endpoint queries the role definition.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `GET` | `/database/roles/:name` | `200 application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the role to read. This
|
||||||
|
is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
https://vault.rocks/v1/database/roles/my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"creation_statements": "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
|
||||||
|
"db_name": "mysql",
|
||||||
|
"default_ttl": 3600,
|
||||||
|
"max_ttl": 86400,
|
||||||
|
"renew_statements": "",
|
||||||
|
"revocation_statements": "",
|
||||||
|
"rollback_statements": ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## List Roles
|
||||||
|
|
||||||
|
This endpoint returns a list of available roles. Only the role names are
|
||||||
|
returned, not any values.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `LIST` | `/database/roles` | `200 application/json` |
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request LIST \
|
||||||
|
https://vault.rocks/v1/database/roles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth": null,
|
||||||
|
"data": {
|
||||||
|
"keys": ["dev", "prod"]
|
||||||
|
},
|
||||||
|
"lease_duration": 2764800,
|
||||||
|
"lease_id": "",
|
||||||
|
"renewable": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete Role
|
||||||
|
|
||||||
|
This endpoint deletes the role definition.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `DELETE` | `/database/roles/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the role to delete. This
|
||||||
|
is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request DELETE \
|
||||||
|
https://vault.rocks/v1/database/roles/my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generate Credentials
|
||||||
|
|
||||||
|
This endpoint generates a new set of dynamic credentials based on the named
|
||||||
|
role.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `GET` | `/database/creds/:name` | `200 application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the role to create
|
||||||
|
credentials against. This is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
https://vault.rocks/v1/database/creds/my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"username": "root-1430158508-126",
|
||||||
|
"password": "132ae3ef-5a64-7499-351e-bfe59f3a2a21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
60
website/source/api/secret/databases/mssql.html.md
Normal file
60
website/source/api/secret/databases/mssql.html.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "MSSQL Database Plugin - HTTP API"
|
||||||
|
sidebar_current: "docs-http-secret-databases-mssql-maria"
|
||||||
|
description: |-
|
||||||
|
The MSSQL plugin for Vault's Database backend generates database credentials to access MSSQL servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MSSQL Database Plugin HTTP API
|
||||||
|
|
||||||
|
The MSSQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the MSSQL database.
|
||||||
|
|
||||||
|
## Configure Connection
|
||||||
|
|
||||||
|
In addition to the parameters defined by the [Database
|
||||||
|
Backend](/api/secret/databases/index.html#configure-connection), this plugin
|
||||||
|
has a number of parameters to further configure a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- `connection_url` `(string: <required>)` - Specifies the MSSQL DSN.
|
||||||
|
|
||||||
|
- `max_open_connections` `(int: 2)` - Speficies the name of the plugin to use
|
||||||
|
for this connection.
|
||||||
|
|
||||||
|
- `max_idle_connections` `(int: 0)` - Specifies the maximum number of idle
|
||||||
|
connections to the database. A zero uses the value of `max_open_connections`
|
||||||
|
and a negative value disables idle connections. If larger than
|
||||||
|
`max_open_connections` it will be reduced to be equal.
|
||||||
|
|
||||||
|
- `max_connection_lifetime` `(string: "0s")` - Specifies the maximum amount of
|
||||||
|
time a connection may be reused. If <= 0s connections are reused forever.
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin_name": "mssql-database-plugin",
|
||||||
|
"allowed_roles": "readonly",
|
||||||
|
"connection_url": "sqlserver://sa:yourStrong(!)Password@localhost:1433",
|
||||||
|
"max_open_connections": 5,
|
||||||
|
"max_connection_lifetime": "5s",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/database/config/mssql
|
||||||
|
```
|
||||||
|
|
60
website/source/api/secret/databases/mysql-maria.html.md
Normal file
60
website/source/api/secret/databases/mysql-maria.html.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "MySQL/MariaDB Database Plugin - HTTP API"
|
||||||
|
sidebar_current: "docs-http-secret-databases-mysql-maria"
|
||||||
|
description: |-
|
||||||
|
The MySQL/MariaDB plugin for Vault's Database backend generates database credentials to access MySQL and MariaDB servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MySQL/MariaDB Database Plugin HTTP API
|
||||||
|
|
||||||
|
The MySQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the MySQL database.
|
||||||
|
|
||||||
|
## Configure Connection
|
||||||
|
|
||||||
|
In addition to the parameters defined by the [Database
|
||||||
|
Backend](/api/secret/databases/index.html#configure-connection), this plugin
|
||||||
|
has a number of parameters to further configure a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- `connection_url` `(string: <required>)` - Specifies the MySQL DSN.
|
||||||
|
|
||||||
|
- `max_open_connections` `(int: 2)` - Speficies the name of the plugin to use
|
||||||
|
for this connection.
|
||||||
|
|
||||||
|
- `max_idle_connections` `(int: 0)` - Specifies the maximum number of idle
|
||||||
|
connections to the database. A zero uses the value of `max_open_connections`
|
||||||
|
and a negative value disables idle connections. If larger than
|
||||||
|
`max_open_connections` it will be reduced to be equal.
|
||||||
|
|
||||||
|
- `max_connection_lifetime` `(string: "0s")` - Specifies the maximum amount of
|
||||||
|
time a connection may be reused. If <= 0s connections are reused forever.
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin_name": "mysql-database-plugin",
|
||||||
|
"allowed_roles": "readonly",
|
||||||
|
"connection_url": "root:mysql@tcp(127.0.0.1:3306)/"
|
||||||
|
"max_open_connections": 5,
|
||||||
|
"max_connection_lifetime": "5s",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/database/config/mysql
|
||||||
|
```
|
||||||
|
|
60
website/source/api/secret/databases/postgresql.html.md
Normal file
60
website/source/api/secret/databases/postgresql.html.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "PostgreSQL Database Plugin - HTTP API"
|
||||||
|
sidebar_current: "docs-http-secret-databases-postgresql-maria"
|
||||||
|
description: |-
|
||||||
|
The PostgreSQL plugin for Vault's Database backend generates database credentials to access PostgreSQL servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PostgreSQL Database Plugin HTTP API
|
||||||
|
|
||||||
|
The PostgreSQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the PostgreSQL database.
|
||||||
|
|
||||||
|
## Configure Connection
|
||||||
|
|
||||||
|
In addition to the parameters defined by the [Database
|
||||||
|
Backend](/api/secret/databases/index.html#configure-connection), this plugin
|
||||||
|
has a number of parameters to further configure a connection.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `POST` | `/database/config/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- `connection_url` `(string: <required>)` - Specifies the PostgreSQL DSN.
|
||||||
|
|
||||||
|
- `max_open_connections` `(int: 2)` - Speficies the name of the plugin to use
|
||||||
|
for this connection.
|
||||||
|
|
||||||
|
- `max_idle_connections` `(int: 0)` - Specifies the maximum number of idle
|
||||||
|
connections to the database. A zero uses the value of `max_open_connections`
|
||||||
|
and a negative value disables idle connections. If larger than
|
||||||
|
`max_open_connections` it will be reduced to be equal.
|
||||||
|
|
||||||
|
- `max_connection_lifetime` `(string: "0s")` - Specifies the maximum amount of
|
||||||
|
time a connection may be reused. If <= 0s connections are reused forever.
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"allowed_roles": "readonly",
|
||||||
|
"connection_url": "postgresql://root:root@localhost:5432/postgres",
|
||||||
|
"max_open_connections": 5,
|
||||||
|
"max_connection_lifetime": "5s",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/database/config/postgresql
|
||||||
|
```
|
||||||
|
|
155
website/source/api/system/plugins-catalog.html.md
Normal file
155
website/source/api/system/plugins-catalog.html.md
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
---
|
||||||
|
layout: "api"
|
||||||
|
page_title: "/sys/plugins/catalog - HTTP API"
|
||||||
|
sidebar_current: "docs-http-system-plugins-catalog"
|
||||||
|
description: |-
|
||||||
|
The `/sys/plugins/catalog` endpoint is used to manage plugins.
|
||||||
|
---
|
||||||
|
|
||||||
|
# `/sys/plugins/catalog`
|
||||||
|
|
||||||
|
The `/sys/plugins/catalog` endpoint is used to list, register, update, and
|
||||||
|
remove plugins in Vault's catalog. Plugins must be registered before use, and
|
||||||
|
once registered backends can use the plugin by querying the catalog.
|
||||||
|
|
||||||
|
## List Plugins
|
||||||
|
|
||||||
|
This endpoint lists the plugins in the catalog.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `LIST` | `/sys/plugins/catalog/` | `200 application/json` |
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request LIST
|
||||||
|
https://vault.rocks/v1/sys/plugins/catalog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"keys": [
|
||||||
|
"cassandra-database-plugin",
|
||||||
|
"mssql-database-plugin",
|
||||||
|
"mysql-database-plugin",
|
||||||
|
"postgresql-database-plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Register Plugin
|
||||||
|
|
||||||
|
This endpoint registers a new plugin, or updates an existing one with the
|
||||||
|
supplied name.
|
||||||
|
|
||||||
|
- **`sudo` required** – This endpoint requires `sudo` capability in addition to
|
||||||
|
any path-specific capabilities.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `PUT` | `/sys/plugins/catalog/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name for this plugin. The name
|
||||||
|
is what is used to look up plugins in the catalog. This is part of the request
|
||||||
|
URL.
|
||||||
|
|
||||||
|
- `sha_256` `(string: <required>)` – This is the SHA256 sum of the plugin's
|
||||||
|
binary. Before a plugin is run it's SHA will be checked against this value, if
|
||||||
|
they do not match the plugin can not be run.
|
||||||
|
|
||||||
|
- `command` `(string: <required>)` – Specifies the command used to execute the
|
||||||
|
plugin. This is relative to the plugin directory. e.g. `"myplugin
|
||||||
|
--my_flag=1"`
|
||||||
|
|
||||||
|
### Sample Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sha_256": "d130b9a0fbfddef9709d8ff92e5e6053ccd246b78632fc03b8548457026961e9",
|
||||||
|
"command": "mysql-database-plugin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request PUT \
|
||||||
|
--data @payload.json \
|
||||||
|
https://vault.rocks/v1/sys/plugins/catalog/example-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read Plugin
|
||||||
|
|
||||||
|
This endpoint returns the configuration data for the plugin with the given name.
|
||||||
|
|
||||||
|
- **`sudo` required** – This endpoint requires `sudo` capability in addition to
|
||||||
|
any path-specific capabilities.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `GET` | `/sys/plugins/catalog/:name` | `200 application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the plugin to retrieve.
|
||||||
|
This is part of the request URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request GET \
|
||||||
|
https://vault.rocks/v1/sys/plugins/catalog/example-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"plugin": {
|
||||||
|
"args": [],
|
||||||
|
"builtin": false,
|
||||||
|
"command": "/tmp/vault-plugins/mysql-database-plugin",
|
||||||
|
"name": "example-plugin",
|
||||||
|
"sha256": "0TC5oPv93vlwnY/5Ll5gU8zSRreGMvwDuFSEVwJpYek="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Remove Plugin from Catalog
|
||||||
|
|
||||||
|
This endpoint removes the plugin with the given name.
|
||||||
|
|
||||||
|
- **`sudo` required** – This endpoint requires `sudo` capability in addition to
|
||||||
|
any path-specific capabilities.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
| :------- | :--------------------------- | :--------------------- |
|
||||||
|
| `DELETE` | `/sys/plugins/catalog/:name` | `204 (empty body)` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – Specifies the name of the plugin to delete.
|
||||||
|
This is part of the request URL.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request DELETE \
|
||||||
|
https://vault.rocks/v1/sys/plugins/catalog/example-plugin
|
||||||
|
```
|
105
website/source/docs/internals/plugins.html.md
Normal file
105
website/source/docs/internals/plugins.html.md
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Plugin System"
|
||||||
|
sidebar_current: "docs-internals-plugins"
|
||||||
|
description: |-
|
||||||
|
Learn about Vault's plugin system.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plugin System
|
||||||
|
Certain Vault backends utilize plugins to extend their functionality outside of
|
||||||
|
what is available in the core vault code. Often times these backends will
|
||||||
|
provide both builtin plugins and a mechanism for executing external plugins.
|
||||||
|
Builtin plugins are shipped with vault, often for commonly used implementations,
|
||||||
|
and require no additional operator intervention to run. Builtin plugins are
|
||||||
|
just like any other backend code inside vault. External plugins, on the other
|
||||||
|
hand, are not shipped with the vault binary and must be registered to vault by
|
||||||
|
a privileged vault user. This section of the documentation will describe the
|
||||||
|
architecture and security of external plugins.
|
||||||
|
|
||||||
|
# Plugin Architecture
|
||||||
|
Vault's plugins are completely separate, standalone applications that Vault
|
||||||
|
executes and communicates with over RPC. This means the plugin process does not
|
||||||
|
share the same memory space as Vault and therefore can only access the
|
||||||
|
interfaces and arguments given to it. This also means a crash in a plugin can not
|
||||||
|
crash the entirety of Vault.
|
||||||
|
|
||||||
|
## Plugin Communication
|
||||||
|
Vault creates a mutually authenticated TLS connection for communication with the
|
||||||
|
plugin's RPC server. While invoking the plugin process Vault passes a [wrapping
|
||||||
|
token](https://www.vaultproject.io/docs/concepts/response-wrapping.html) to the
|
||||||
|
plugin process' environment. This token is single use and has a short TTL. Once
|
||||||
|
unwrapped, it provides the plugin with a unique generated TLS certificate and
|
||||||
|
private key for it to use to talk to the original vault process.
|
||||||
|
|
||||||
|
## Plugin Registration
|
||||||
|
An important consideration of Vault's plugin system is to ensure the plugin
|
||||||
|
invoked by vault is authentic and maintains integrity. There are two components
|
||||||
|
that a Vault operator needs to configure before external plugins can be run, the
|
||||||
|
plugin directory and the plugin catalog entry.
|
||||||
|
|
||||||
|
### Plugin Directory
|
||||||
|
The plugin directory is a configuration option of Vault, and can be specified in
|
||||||
|
the [configuration file](https://www.vaultproject.io/docs/configuration/index.html).
|
||||||
|
This setting specifies a directory that all plugin binaries must live. A plugin
|
||||||
|
can not be added to vault unless it exists in the plugin directory. There is no
|
||||||
|
default for this configuration option, and if it is not set plugins can not be
|
||||||
|
added to vault.
|
||||||
|
|
||||||
|
~> Warning: A vault operator should take care to lock down the permissions on
|
||||||
|
this directory to ensure a plugin can not be modified by an unauthorized user
|
||||||
|
between the time of the SHA check and the time of plugin execution.
|
||||||
|
|
||||||
|
### Plugin Catalog
|
||||||
|
The plugin catalog is Vault's list of approved plugins. The catalog is stored in
|
||||||
|
Vault's barrier and can only be updated by a vault user with sudo permissions.
|
||||||
|
Upon adding a new plugin, the plugin name, SHA256 sum of the executable, and the
|
||||||
|
command that should be used to run the plugin must be provided. The catalog will
|
||||||
|
make sure the executable referenced in the command exists in the plugin
|
||||||
|
directory. When added to the catalog the plugin is not automatically executed,
|
||||||
|
it instead becomes visible to backends and can be executed by them.
|
||||||
|
|
||||||
|
### Plugin Execution
|
||||||
|
When a backend wants to run a plugin, it first looks up the plugin, by name, in
|
||||||
|
the catalog. It then checks the executable's SHA256 sum against the one
|
||||||
|
configured in the plugin catalog. Finally vault runs the command configured in
|
||||||
|
the catalog, sending along the JWT formatted response wrapping token and mlock
|
||||||
|
settings (like Vault, plugins support the use of mlock when availible).
|
||||||
|
|
||||||
|
# Plugin Development
|
||||||
|
Because Vault communicates to plugins over a RPC interface, you can build and
|
||||||
|
distribute a plugin for Vault without having to rebuild Vault itself. This makes
|
||||||
|
it easy for you to build a Vault plugin for your organization's internal use,
|
||||||
|
for a proprietary API that you don't want to open source, or to prototype
|
||||||
|
something before contributing it back to the main project.
|
||||||
|
|
||||||
|
In theory, because the plugin interface is HTTP, you could even develop a plugin
|
||||||
|
using a completely different programming language! (Disclaimer, you would also
|
||||||
|
have to re-implement the plugin API which is not a trivial amount of work.)
|
||||||
|
|
||||||
|
~> Advanced topic! Plugin development is a highly advanced topic in Vault, and
|
||||||
|
is not required knowledge for day-to-day usage. If you don't plan on writing any
|
||||||
|
plugins, we recommend not reading this section of the documentation.
|
||||||
|
|
||||||
|
Developing a plugin is simple. The only knowledge necessary to write
|
||||||
|
a plugin is basic command-line skills and basic knowledge of the
|
||||||
|
[Go programming language](http://golang.org).
|
||||||
|
|
||||||
|
You're plugin implementation just needs to satisfy the interface for the plugin
|
||||||
|
type you want to build. You can find these definitions in the docs for the
|
||||||
|
backend running the plugin.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugins.Serve(new(MyPlugin), nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's basically it! You would just need to change MyPlugin to your actual
|
||||||
|
plugin.
|
62
website/source/docs/secrets/databases/cassandra.html.md
Normal file
62
website/source/docs/secrets/databases/cassandra.html.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Cassandra Database Plugin"
|
||||||
|
sidebar_current: "docs-secrets-databases-cassandra"
|
||||||
|
description: |-
|
||||||
|
The Cassandra plugin for Vault's Database backend generates database credentials to access Cassandra.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cassandra Database Plugin
|
||||||
|
|
||||||
|
Name: `cassandra-database-plugin`
|
||||||
|
|
||||||
|
The Cassandra Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the Cassandra database.
|
||||||
|
|
||||||
|
See the [Database Backend](/docs/secrets/databases/index.html) docs for more
|
||||||
|
information about setting up the Database Backend.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
After the Database Backend is mounted you can configure a cassandra connection
|
||||||
|
by specifying this plugin as the `"plugin_name"` argument. Here is an example
|
||||||
|
cassandra configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/cassandra \
|
||||||
|
plugin_name=cassandra-database-plugin \
|
||||||
|
allowed_roles="readonly" \
|
||||||
|
hosts=localhost \
|
||||||
|
username=cassandra \
|
||||||
|
password=cassandra
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the cassandra connection is configured we can add a role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/roles/readonly \
|
||||||
|
db_name=cassandra \
|
||||||
|
creation_statements="CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER; \
|
||||||
|
GRANT SELECT ON ALL KEYSPACES TO {{username}};" \
|
||||||
|
default_ttl="1h" \
|
||||||
|
max_ttl="24h"
|
||||||
|
|
||||||
|
|
||||||
|
Success! Data written to: database/roles/readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
This role can be used to retrieve a new set of credentials by querying the
|
||||||
|
"database/creds/readonly" endpoint.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The full list of configurable options can be seen in the [Cassandra database
|
||||||
|
plugin API](/api/secret/databases/cassandra.html) page.
|
||||||
|
|
||||||
|
For more information on the Database secret backend's HTTP API please see the [Database secret
|
||||||
|
backend API](/api/secret/databases/index.html) page.
|
||||||
|
|
121
website/source/docs/secrets/databases/custom.html.md
Normal file
121
website/source/docs/secrets/databases/custom.html.md
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Custom Database Plugins"
|
||||||
|
sidebar_current: "docs-secrets-databases-custom"
|
||||||
|
description: |-
|
||||||
|
Creating custom database plugins for Vault's Database backend to generate credentials for a database.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Custom Database Plugins
|
||||||
|
|
||||||
|
The Database backend allows new functionality to be added through a plugin
|
||||||
|
interface without needing to modify vault's core code. This allows you write
|
||||||
|
your own code to generate credentials in any database you wish. It also allows
|
||||||
|
databases that require dynamically linked libraries to be used as plugins while
|
||||||
|
keeping Vault itself statically linked.
|
||||||
|
|
||||||
|
~> **Advanced topic!** Plugin development is a highly advanced
|
||||||
|
topic in Vault, and is not required knowledge for day-to-day usage.
|
||||||
|
If you don't plan on writing any plugins, we recommend not reading
|
||||||
|
this section of the documentation.
|
||||||
|
|
||||||
|
Please read the [Plugins internals](/docs/internals/plugins.html) docs for more
|
||||||
|
information about the plugin system before getting started building your
|
||||||
|
Database plugin.
|
||||||
|
|
||||||
|
## Plugin Interface
|
||||||
|
|
||||||
|
All plugins for the Database backend must implement the same simple interface.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Database interface {
|
||||||
|
Type() (string, error)
|
||||||
|
CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error)
|
||||||
|
RenewUser(statements Statements, username string, expiration time.Time) error
|
||||||
|
RevokeUser(statements Statements, username string) error
|
||||||
|
|
||||||
|
Initialize(config map[string]interface{}, verifyConnection bool) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll notice the first parameter to a number of those functions is a
|
||||||
|
`Statements` struct. This struct is used to pass the Role's configured
|
||||||
|
statements to the plugin on function call. The struct is defined as:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Statements struct {
|
||||||
|
CreationStatements string
|
||||||
|
RevocationStatements string
|
||||||
|
RollbackStatements string
|
||||||
|
RenewStatements string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is up to your plugin to replace the `{{name}}`, `{{password}}`, and
|
||||||
|
`{{expiration}}` in these statements with the proper vaules.
|
||||||
|
|
||||||
|
The `Initialize` function is passed a map of keys to values, this data is what the
|
||||||
|
user specified as the configuration for the plugin. Your plugin should use this
|
||||||
|
data to make connections to the database. It is also passed a boolean value
|
||||||
|
specifying whether or not your plugin should return an error if it is unable to
|
||||||
|
connect to the database.
|
||||||
|
|
||||||
|
## Serving your plugin
|
||||||
|
|
||||||
|
Once your plugin is built you should pass it to vault's `plugins` package by
|
||||||
|
calling the `Serve` method:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/vault/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugins.Serve(new(MyPlugin), nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replacing `MyPlugin` with the actual implementation of your plugin.
|
||||||
|
|
||||||
|
The second parameter to `Serve` takes in an optional vault `api.TLSConfig` for
|
||||||
|
configuring the plugin to communicate with vault for the initial unwrap call.
|
||||||
|
This is useful if your vault setup requires client certificate checks. This
|
||||||
|
config wont be used once the plugin unwraps its own TLS cert and key.
|
||||||
|
|
||||||
|
## Running your plugin
|
||||||
|
|
||||||
|
The above main package, once built, will supply you with a binary of your
|
||||||
|
plugin. We also recommend if you are planning on distributing your plugin to
|
||||||
|
build with [gox](https://github.com/mitchellh/gox) for cross platform builds.
|
||||||
|
|
||||||
|
To use your plugin with the Database backend you need to place the binary in the
|
||||||
|
plugin directory as specified in the [plugin internals](/docs/internals/plugins.html) docs.
|
||||||
|
|
||||||
|
You should now be able to register your plugin into the vault catalog. To do
|
||||||
|
this your token will need sudo permissions.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write sys/plugins/catalog/myplugin-database-plugin \
|
||||||
|
sha_256=<expected SHA256 Hex value of the plugin binary> \
|
||||||
|
command="myplugin"
|
||||||
|
Success! Data written to: sys/plugins/catalog/myplugin-database-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you should be able to configure your plugin like any other:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/myplugin \
|
||||||
|
plugin_name=myplugin-database-plugin \
|
||||||
|
allowed_roles="readonly" \
|
||||||
|
myplugins_connection_details=....
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
97
website/source/docs/secrets/databases/index.html.md
Normal file
97
website/source/docs/secrets/databases/index.html.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Databases"
|
||||||
|
sidebar_current: "docs-secrets-databases"
|
||||||
|
description: |-
|
||||||
|
Top page for database secret backend information
|
||||||
|
---
|
||||||
|
|
||||||
|
# Databases
|
||||||
|
|
||||||
|
Name: `Database`
|
||||||
|
|
||||||
|
The Database secret backend for Vault generates database credentials dynamically
|
||||||
|
based on configured roles. It works with a number of different databases through
|
||||||
|
a plugin interface. There are a number of builtin database types and an exposed
|
||||||
|
framework for running custom database types for extendability. This means that
|
||||||
|
services that need to access a database no longer need to hardcode credentials:
|
||||||
|
they can request them from Vault, and use Vault's leasing mechanism to more
|
||||||
|
easily roll keys.
|
||||||
|
|
||||||
|
Additionally, it introduces a new ability: with every service accessing the
|
||||||
|
database with unique credentials, it makes auditing much easier when
|
||||||
|
questionable data access is discovered: you can track it down to the specific
|
||||||
|
instance of a service based on the SQL username.
|
||||||
|
|
||||||
|
Vault makes use of its own internal revocation system to ensure that users
|
||||||
|
become invalid within a reasonable time of the lease expiring.
|
||||||
|
|
||||||
|
This page will show a quick start for this backend. For detailed documentation
|
||||||
|
on every path, use vault path-help after mounting the backend.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
The first step in using the Database backend is mounting it.
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ vault mount database
|
||||||
|
Successfully mounted 'database' at 'database'!
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we must configure this backend to connect to a database. In this example
|
||||||
|
we will connect to a MySQL database, but the configuration details needed for
|
||||||
|
other plugin types can be found in their docs pages. This backend can configure
|
||||||
|
multiple database connections, therefore a name for the connection must be
|
||||||
|
provide; we'll call this one simply "mysql".
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/mysql \
|
||||||
|
plugin_name=mysql-database-plugin \
|
||||||
|
connection_url="root:mysql@tcp(127.0.0.1:3306)/" \
|
||||||
|
allowed_roles="readonly"
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
The next step is to configure a role. A role is a logical name that maps to a
|
||||||
|
policy used to generate those credentials. A role needs to be configured with
|
||||||
|
the database name we created above, and the default/max TTLs. For example, lets
|
||||||
|
create a "readonly" role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/roles/readonly \
|
||||||
|
db_name=mysql \
|
||||||
|
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" \
|
||||||
|
default_ttl="1h" \
|
||||||
|
max_ttl="24h"
|
||||||
|
Success! Data written to: database/roles/readonly
|
||||||
|
```
|
||||||
|
By writing to the roles/readonly path we are defining the readonly role. This
|
||||||
|
role will be created by evaluating the given creation statements. By default,
|
||||||
|
the {{name}} and {{password}} fields will be populated by the plugin with
|
||||||
|
dynamically generated values. In other plugins the {{expiration}} field could
|
||||||
|
also be supported. This SQL statement is creating the named user, and then
|
||||||
|
granting it SELECT or read-only privileges to tables in the database. More
|
||||||
|
complex GRANT queries can be used to customize the privileges of the role.
|
||||||
|
Custom revocation statements could be passed too, but this plugin has a default
|
||||||
|
statement we can use.
|
||||||
|
|
||||||
|
To generate a new set of credentials, we simply read from that role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault read database/creds/readonly
|
||||||
|
Key Value
|
||||||
|
--- -----
|
||||||
|
lease_id database/creds/readonly/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6
|
||||||
|
lease_duration 1h0m0s
|
||||||
|
lease_renewable true
|
||||||
|
password 8cab931c-d62e-a73d-60d3-5ee85139cd66
|
||||||
|
username v-root-e2978cd0-
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The Database secret backend has a full HTTP API. Please see the [Database secret
|
||||||
|
backend API](/api/secret/databases/index.html) for more details.
|
||||||
|
|
60
website/source/docs/secrets/databases/mssql.html.md
Normal file
60
website/source/docs/secrets/databases/mssql.html.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "MSSQL Database Plugin"
|
||||||
|
sidebar_current: "docs-secrets-databases-mssql"
|
||||||
|
description: |-
|
||||||
|
The MSSQL plugin for Vault's Database backend generates database credentials to access Microsoft SQL Server.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MSSQL Database Plugin
|
||||||
|
|
||||||
|
Name: `mssql-database-plugin`
|
||||||
|
|
||||||
|
The MSSQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the MSSQL database.
|
||||||
|
|
||||||
|
See the [Database Backend](/docs/secrets/databases/index.html) docs for more
|
||||||
|
information about setting up the Database Backend.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
After the Database Backend is mounted you can configure a MSSQL connection
|
||||||
|
by specifying this plugin as the `"plugin_name"` argument. Here is an example
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/mssql \
|
||||||
|
plugin_name=mssql-database-plugin \
|
||||||
|
connection_url='sqlserver://sa:yourStrong(!)Password@localhost:1433' \
|
||||||
|
allowed_roles="readonly"
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the MSSQL connection is configured we can add a role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/roles/readonly \
|
||||||
|
db_name=mssql \
|
||||||
|
creation_statements="CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';\
|
||||||
|
CREATE USER [{{name}}] FOR LOGIN [{{name}}];\
|
||||||
|
GRANT SELECT ON SCHEMA::dbo TO [{{name}}];" \
|
||||||
|
default_ttl="1h" \
|
||||||
|
max_ttl="24h"
|
||||||
|
|
||||||
|
Success! Data written to: database/roles/readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
This role can now be used to retrieve a new set of credentials by querying the
|
||||||
|
"database/creds/readonly" endpoint.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The full list of configurable options can be seen in the [MSSQL database
|
||||||
|
plugin API](/api/secret/databases/mssql.html) page.
|
||||||
|
|
||||||
|
For more information on the Database secret backend's HTTP API please see the [Database secret
|
||||||
|
backend API](/api/secret/databases/index.html) page.
|
||||||
|
|
69
website/source/docs/secrets/databases/mysql-maria.html.md
Normal file
69
website/source/docs/secrets/databases/mysql-maria.html.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "MySQL/MariaDB Database Plugin"
|
||||||
|
sidebar_current: "docs-secrets-databases-mysql-maria"
|
||||||
|
description: |-
|
||||||
|
The MySQL/MariaDB plugin for Vault's Database backend generates database credentials to access MySQL and MariaDB servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MySQL/MariaDB Database Plugin
|
||||||
|
|
||||||
|
Name: `mysql-database-plugin`, `mysql-aurora-database-plugin`, `mysql-rds-database-plugin`,
|
||||||
|
`mysql-legacy-database-plugin`
|
||||||
|
|
||||||
|
The MySQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the MySQL database.
|
||||||
|
|
||||||
|
This plugin has a few different instances built into vault, each instance is for
|
||||||
|
a slightly different MySQL driver. The only difference between these plugins is
|
||||||
|
the length of usernames generated by the plugin as different versions of mysql
|
||||||
|
accept different lengths. The availible plugins are:
|
||||||
|
|
||||||
|
- mysql-database-plugin
|
||||||
|
- mysql-aurora-database-plugin
|
||||||
|
- mysql-rds-database-plugin
|
||||||
|
- mysql-legacy-database-plugin
|
||||||
|
|
||||||
|
See the [Database Backend](/docs/secrets/databases/index.html) docs for more
|
||||||
|
information about setting up the Database Backend.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
After the Database Backend is mounted you can configure a MySQL connection
|
||||||
|
by specifying this plugin as the `"plugin_name"` argument. Here is an example
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/mysql \
|
||||||
|
plugin_name=mysql-database-plugin \
|
||||||
|
connection_url="root:mysql@tcp(127.0.0.1:3306)/" \
|
||||||
|
allowed_roles="readonly"
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the MySQL connection is configured we can add a role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/roles/readonly \
|
||||||
|
db_name=mysql \
|
||||||
|
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" \
|
||||||
|
default_ttl="1h" \
|
||||||
|
max_ttl="24h"
|
||||||
|
|
||||||
|
Success! Data written to: database/roles/readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
This role can now be used to retrieve a new set of credentials by querying the
|
||||||
|
"database/creds/readonly" endpoint.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The full list of configurable options can be seen in the [MySQL database
|
||||||
|
plugin API](/api/secret/databases/mysql-maria.html) page.
|
||||||
|
|
||||||
|
For more information on the Database secret backend's HTTP API please see the [Database secret
|
||||||
|
backend API](/api/secret/databases/index.html) page.
|
||||||
|
|
60
website/source/docs/secrets/databases/postgresql.html.md
Normal file
60
website/source/docs/secrets/databases/postgresql.html.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "PostgreSQL Database Plugin"
|
||||||
|
sidebar_current: "docs-secrets-databases-postgresql"
|
||||||
|
description: |-
|
||||||
|
The PostgreSQL plugin for Vault's Database backend generates database credentials to access PostgreSQL.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PostgreSQL Database Plugin
|
||||||
|
|
||||||
|
Name: `postgresql-database-plugin`
|
||||||
|
|
||||||
|
The PostgreSQL Database Plugin is one of the supported plugins for the Database
|
||||||
|
backend. This plugin generates database credentials dynamically based on
|
||||||
|
configured roles for the PostgreSQL database.
|
||||||
|
|
||||||
|
See the [Database Backend](/docs/secrets/databases/index.html) docs for more
|
||||||
|
information about setting up the Database Backend.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
After the Database Backend is mounted you can configure a PostgreSQL connection
|
||||||
|
by specifying this plugin as the `"plugin_name"` argument. Here is an example
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/config/postgresql \
|
||||||
|
plugin_name=postgresql-database-plugin \
|
||||||
|
allowed_roles="readonly" \
|
||||||
|
connection_url="postgresql://root:root@localhost:5432/"
|
||||||
|
|
||||||
|
The following warnings were returned from the Vault server:
|
||||||
|
* Read access to this endpoint should be controlled via ACLs as it will return the connection details as is, including passwords, if any.
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the PostgreSQL connection is configured we can add a role. The PostgreSQL
|
||||||
|
plugin replaces `{{expiration}}` in statements with a formated timestamp:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write database/roles/readonly \
|
||||||
|
db_name=postgresql \
|
||||||
|
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
|
||||||
|
default_ttl="1h" \
|
||||||
|
max_ttl="24h"
|
||||||
|
|
||||||
|
Success! Data written to: database/roles/readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
This role can be used to retrieve a new set of credentials by querying the
|
||||||
|
"database/creds/readonly" endpoint.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The full list of configurable options can be seen in the [PostgreSQL database
|
||||||
|
plugin API](/api/secret/databases/postgresql.html) page.
|
||||||
|
|
||||||
|
For more information on the Database secret backend's HTTP API please see the [Database secret
|
||||||
|
backend API](/api/secret/databases/index.html) page.
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<a href="/api/secret/aws/index.html">AWS</a>
|
<a href="/api/secret/aws/index.html">AWS</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-cassandra") %>>
|
<li<%= sidebar_current("docs-http-secret-cassandra") %>>
|
||||||
<a href="/api/secret/cassandra/index.html">Cassandra</a>
|
<a href="/api/secret/cassandra/index.html">Cassandra (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-consul") %>>
|
<li<%= sidebar_current("docs-http-secret-consul") %>>
|
||||||
<a href="/api/secret/consul/index.html">Consul</a>
|
<a href="/api/secret/consul/index.html">Consul</a>
|
||||||
|
@ -29,6 +29,25 @@
|
||||||
<li<%= sidebar_current("docs-http-secret-cubbyhole") %>>
|
<li<%= sidebar_current("docs-http-secret-cubbyhole") %>>
|
||||||
<a href="/api/secret/cubbyhole/index.html">Cubbyhole</a>
|
<a href="/api/secret/cubbyhole/index.html">Cubbyhole</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-http-secret-databases") %>>
|
||||||
|
<a href="/api/secret/databases/index.html">Databases (Beta)</a>
|
||||||
|
<ul class="nav">
|
||||||
|
<li<%= sidebar_current("docs-http-secret-databases-cassandra") %>>
|
||||||
|
<a href="/api/secret/databases/cassandra.html">Cassandra</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-http-secret-databases-mssql") %>>
|
||||||
|
<a href="/api/secret/databases/mssql.html">MSSQL</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-http-secret-databases-mysql-maria") %>>
|
||||||
|
<a href="/api/secret/databases/mysql-maria.html">MySQL/MariaDB</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-http-secret-databases-postgresql") %>>
|
||||||
|
<a href="/api/secret/databases/postgresql.html">PostgreSQL</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-http-secret-generic") %>>
|
<li<%= sidebar_current("docs-http-secret-generic") %>>
|
||||||
<a href="/api/secret/generic/index.html">Generic</a>
|
<a href="/api/secret/generic/index.html">Generic</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -36,16 +55,16 @@
|
||||||
<a href="/api/secret/mongodb/index.html">MongoDB</a>
|
<a href="/api/secret/mongodb/index.html">MongoDB</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-mssql") %>>
|
<li<%= sidebar_current("docs-http-secret-mssql") %>>
|
||||||
<a href="/api/secret/mssql/index.html">MSSQL</a>
|
<a href="/api/secret/mssql/index.html">MSSQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-mysql") %>>
|
<li<%= sidebar_current("docs-http-secret-mysql") %>>
|
||||||
<a href="/api/secret/mysql/index.html">MySQL</a>
|
<a href="/api/secret/mysql/index.html">MySQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-pki") %>>
|
<li<%= sidebar_current("docs-http-secret-pki") %>>
|
||||||
<a href="/api/secret/pki/index.html">PKI</a>
|
<a href="/api/secret/pki/index.html">PKI</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-postgresql") %>>
|
<li<%= sidebar_current("docs-http-secret-postgresql") %>>
|
||||||
<a href="/api/secret/postgresql/index.html">PostgreSQL</a>
|
<a href="/api/secret/postgresql/index.html">PostgreSQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-secret-rabbitmq") %>>
|
<li<%= sidebar_current("docs-http-secret-rabbitmq") %>>
|
||||||
<a href="/api/secret/rabbitmq/index.html">RabbitMQ</a>
|
<a href="/api/secret/rabbitmq/index.html">RabbitMQ</a>
|
||||||
|
@ -107,6 +126,9 @@
|
||||||
<li<%= sidebar_current("docs-http-system-mounts") %>>
|
<li<%= sidebar_current("docs-http-system-mounts") %>>
|
||||||
<a href="/api/system/mounts.html"><tt>/sys/mounts</tt></a>
|
<a href="/api/system/mounts.html"><tt>/sys/mounts</tt></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-http-system-plugins-catalog") %>>
|
||||||
|
<a href="/api/system/plugins-catalog.html"><tt>/sys/plugins/catalog</tt></a>
|
||||||
|
</li>
|
||||||
<li<%= sidebar_current("docs-http-system-policy") %>>
|
<li<%= sidebar_current("docs-http-system-policy") %>>
|
||||||
<a href="/api/system/policy.html"><tt>/sys/policy</tt></a>
|
<a href="/api/system/policy.html"><tt>/sys/policy</tt></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -35,6 +35,10 @@
|
||||||
<li<%= sidebar_current("docs-internals-replication") %>>
|
<li<%= sidebar_current("docs-internals-replication") %>>
|
||||||
<a href="/docs/internals/replication.html">Replication</a>
|
<a href="/docs/internals/replication.html">Replication</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-internals-plugins") %>>
|
||||||
|
<a href="/docs/internals/plugins.html">Plugins</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -204,7 +208,7 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-cassandra") %>>
|
<li<%= sidebar_current("docs-secrets-cassandra") %>>
|
||||||
<a href="/docs/secrets/cassandra/index.html">Cassandra</a>
|
<a href="/docs/secrets/cassandra/index.html">Cassandra (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-consul") %>>
|
<li<%= sidebar_current("docs-secrets-consul") %>>
|
||||||
|
@ -215,6 +219,27 @@
|
||||||
<a href="/docs/secrets/cubbyhole/index.html">Cubbyhole</a>
|
<a href="/docs/secrets/cubbyhole/index.html">Cubbyhole</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases") %>>
|
||||||
|
<a href="/docs/secrets/databases/index.html">Databases (Beta)</a>
|
||||||
|
<ul class="nav">
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases-cassandra") %>>
|
||||||
|
<a href="/docs/secrets/databases/cassandra.html">Cassandra</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases-mssql") %>>
|
||||||
|
<a href="/docs/secrets/databases/mssql.html">MSSQL</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases-mysql-maria") %>>
|
||||||
|
<a href="/docs/secrets/databases/mysql-maria.html">MySQL/MariaDB</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases-postgresql") %>>
|
||||||
|
<a href="/docs/secrets/databases/postgresql.html">PostgreSQL</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-secrets-databases-custom") %>>
|
||||||
|
<a href="/docs/secrets/databases/custom.html">Custom</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-generic") %>>
|
<li<%= sidebar_current("docs-secrets-generic") %>>
|
||||||
<a href="/docs/secrets/generic/index.html">Generic</a>
|
<a href="/docs/secrets/generic/index.html">Generic</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -224,11 +249,11 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-mssql") %>>
|
<li<%= sidebar_current("docs-secrets-mssql") %>>
|
||||||
<a href="/docs/secrets/mssql/index.html">MSSQL</a>
|
<a href="/docs/secrets/mssql/index.html">MSSQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-mysql") %>>
|
<li<%= sidebar_current("docs-secrets-mysql") %>>
|
||||||
<a href="/docs/secrets/mysql/index.html">MySQL</a>
|
<a href="/docs/secrets/mysql/index.html">MySQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-pki") %>>
|
<li<%= sidebar_current("docs-secrets-pki") %>>
|
||||||
|
@ -236,7 +261,7 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-postgresql") %>>
|
<li<%= sidebar_current("docs-secrets-postgresql") %>>
|
||||||
<a href="/docs/secrets/postgresql/index.html">PostgreSQL</a>
|
<a href="/docs/secrets/postgresql/index.html">PostgreSQL (Deprecated)</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-secrets-rabbitmq") %>>
|
<li<%= sidebar_current("docs-secrets-rabbitmq") %>>
|
||||||
|
|
Loading…
Reference in a new issue