mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Mongodb plugin (#2698)
* WIP on mongodb plugin * Add mongodb plugin * Add tests * Update mongodb.CreateUser() comment * Update docs * Add missing docs * Fix mongodb docs * Minor comment and test updates * Fix imports * Fix dockertest import * Set c.Initialized at the end, check for empty CreationStmts first on CreateUser * Remove Initialized check on Connection() * Add back Initialized check * Update docs * Move connProducer and credsProducer into pkg for mongodb and cassandra * Chage parseMongoURL to be a private func * Default to admin if no db is provided in creation_statements * Update comments and docs
This commit is contained in:
committed by
GitHub
parent
b203d51068
commit
a4c652cbb3
@@ -29,10 +29,10 @@ type Cassandra struct {
|
||||
|
||||
// New returns a new Cassandra instance
|
||||
func New() (interface{}, error) {
|
||||
connProducer := &connutil.CassandraConnectionProducer{}
|
||||
connProducer := &cassandraConnectionProducer{}
|
||||
connProducer.Type = cassandraTypeName
|
||||
|
||||
credsProducer := &credsutil.CassandraCredentialsProducer{}
|
||||
credsProducer := &cassandraCredentialsProducer{}
|
||||
|
||||
dbType := &Cassandra{
|
||||
ConnectionProducer: connProducer,
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -85,7 +84,7 @@ func TestCassandra_Initialize(t *testing.T) {
|
||||
|
||||
dbRaw, _ := New()
|
||||
db := dbRaw.(*Cassandra)
|
||||
connProducer := db.ConnectionProducer.(*connutil.CassandraConnectionProducer)
|
||||
connProducer := db.ConnectionProducer.(*cassandraConnectionProducer)
|
||||
|
||||
err := db.Initialize(connectionDetails, true)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package connutil
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/helper/tlsutil"
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
)
|
||||
|
||||
// CassandraConnectionProducer implements ConnectionProducer and provides an
|
||||
// cassandraConnectionProducer implements ConnectionProducer and provides an
|
||||
// interface for cassandra databases to make connections.
|
||||
type CassandraConnectionProducer struct {
|
||||
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"`
|
||||
@@ -41,7 +42,7 @@ type CassandraConnectionProducer struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (c *CassandraConnectionProducer) Initialize(conf map[string]interface{}, verifyConnection bool) error {
|
||||
func (c *cassandraConnectionProducer) Initialize(conf map[string]interface{}, verifyConnection bool) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
@@ -49,7 +50,6 @@ func (c *CassandraConnectionProducer) Initialize(conf map[string]interface{}, ve
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Initialized = true
|
||||
|
||||
if c.ConnectTimeoutRaw == nil {
|
||||
c.ConnectTimeoutRaw = "0s"
|
||||
@@ -100,17 +100,22 @@ func (c *CassandraConnectionProducer) Initialize(conf map[string]interface{}, ve
|
||||
c.TLS = true
|
||||
}
|
||||
|
||||
// Set initialized to true at this point since all fields are set,
|
||||
// and the connection can be established at a later time.
|
||||
c.Initialized = true
|
||||
|
||||
if verifyConnection {
|
||||
if _, err := c.Connection(); err != nil {
|
||||
return fmt.Errorf("error Initalizing Connection: %s", err)
|
||||
return fmt.Errorf("error verifying connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CassandraConnectionProducer) Connection() (interface{}, error) {
|
||||
func (c *cassandraConnectionProducer) Connection() (interface{}, error) {
|
||||
if !c.Initialized {
|
||||
return nil, errNotInitialized
|
||||
return nil, connutil.ErrNotInitialized
|
||||
}
|
||||
|
||||
// If we already have a DB, return it
|
||||
@@ -129,7 +134,7 @@ func (c *CassandraConnectionProducer) Connection() (interface{}, error) {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (c *CassandraConnectionProducer) Close() error {
|
||||
func (c *cassandraConnectionProducer) Close() error {
|
||||
// Grab the write lock
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
@@ -143,7 +148,7 @@ func (c *CassandraConnectionProducer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CassandraConnectionProducer) createSession() (*gocql.Session, error) {
|
||||
func (c *cassandraConnectionProducer) createSession() (*gocql.Session, error) {
|
||||
clusterConfig := gocql.NewCluster(strings.Split(c.Hosts, ",")...)
|
||||
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: c.Username,
|
||||
@@ -1,4 +1,4 @@
|
||||
package credsutil
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
)
|
||||
|
||||
// CassandraCredentialsProducer implements CredentialsProducer and provides an
|
||||
// cassandraCredentialsProducer implements CredentialsProducer and provides an
|
||||
// interface for cassandra databases to generate user information.
|
||||
type CassandraCredentialsProducer struct{}
|
||||
type cassandraCredentialsProducer struct{}
|
||||
|
||||
func (ccp *CassandraCredentialsProducer) GenerateUsername(displayName string) (string, error) {
|
||||
func (ccp *cassandraCredentialsProducer) GenerateUsername(displayName string) (string, error) {
|
||||
userUUID, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -23,7 +23,7 @@ func (ccp *CassandraCredentialsProducer) GenerateUsername(displayName string) (s
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func (ccp *CassandraCredentialsProducer) GeneratePassword() (string, error) {
|
||||
func (ccp *cassandraCredentialsProducer) GeneratePassword() (string, error) {
|
||||
password, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -32,6 +32,6 @@ func (ccp *CassandraCredentialsProducer) GeneratePassword() (string, error) {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (ccp *CassandraCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) {
|
||||
func (ccp *cassandraCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
@@ -421,7 +421,7 @@ seed_provider:
|
||||
parameters:
|
||||
# seeds is actually a comma-delimited list of addresses.
|
||||
# Ex: "<ip1>,<ip2>,<ip3>"
|
||||
- seeds: "172.17.0.2"
|
||||
- seeds: "172.17.0.4"
|
||||
|
||||
# For workloads with more data than can fit in memory, Cassandra's
|
||||
# bottleneck will be reads that need to fetch data from
|
||||
@@ -572,7 +572,7 @@ ssl_storage_port: 7001
|
||||
#
|
||||
# Setting listen_address to 0.0.0.0 is always wrong.
|
||||
#
|
||||
listen_address: 172.17.0.2
|
||||
listen_address: 172.17.0.4
|
||||
|
||||
# Set listen_address OR listen_interface, not both. Interfaces must correspond
|
||||
# to a single address, IP aliasing is not supported.
|
||||
@@ -586,7 +586,7 @@ listen_address: 172.17.0.2
|
||||
|
||||
# Address to broadcast to other Cassandra nodes
|
||||
# Leaving this blank will set it to the same value as listen_address
|
||||
broadcast_address: 172.17.0.2
|
||||
broadcast_address: 172.17.0.4
|
||||
|
||||
# When using multiple physical network interfaces, set this
|
||||
# to true to listen on broadcast_address in addition to
|
||||
@@ -668,7 +668,7 @@ rpc_port: 9160
|
||||
# be set to 0.0.0.0. If left blank, this will be set to the value of
|
||||
# rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must
|
||||
# be set.
|
||||
broadcast_rpc_address: 172.17.0.2
|
||||
broadcast_rpc_address: 172.17.0.4
|
||||
|
||||
# enable or disable keepalive on rpc/native connections
|
||||
rpc_keepalive: true
|
||||
|
||||
167
plugins/database/mongodb/connection_producer.go
Normal file
167
plugins/database/mongodb/connection_producer.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
// mongoDBConnectionProducer implements ConnectionProducer and provides an
|
||||
// interface for databases to make connections.
|
||||
type mongoDBConnectionProducer struct {
|
||||
ConnectionURL string `json:"connection_url" structs:"connection_url" mapstructure:"connection_url"`
|
||||
|
||||
Initialized bool
|
||||
Type string
|
||||
session *mgo.Session
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// Initialize parses connection configuration.
|
||||
func (c *mongoDBConnectionProducer) 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")
|
||||
}
|
||||
|
||||
// Set initialized to true at this point since all fields are set,
|
||||
// and the connection can be established at a later time.
|
||||
c.Initialized = true
|
||||
|
||||
if verifyConnection {
|
||||
if _, err := c.Connection(); err != nil {
|
||||
return fmt.Errorf("error verifying connection: %s", err)
|
||||
}
|
||||
|
||||
if err := c.session.Ping(); err != nil {
|
||||
return fmt.Errorf("error verifying connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connection creates a database connection.
|
||||
func (c *mongoDBConnectionProducer) Connection() (interface{}, error) {
|
||||
if !c.Initialized {
|
||||
return nil, connutil.ErrNotInitialized
|
||||
}
|
||||
|
||||
if c.session != nil {
|
||||
return c.session, nil
|
||||
}
|
||||
|
||||
dialInfo, err := parseMongoURL(c.ConnectionURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.session, err = mgo.DialWithInfo(dialInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.session.SetSyncTimeout(1 * time.Minute)
|
||||
c.session.SetSocketTimeout(1 * time.Minute)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Close terminates the database connection.
|
||||
func (c *mongoDBConnectionProducer) Close() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.session != nil {
|
||||
c.session.Close()
|
||||
}
|
||||
|
||||
c.session = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMongoURL(rawURL string) (*mgo.DialInfo, error) {
|
||||
url, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := mgo.DialInfo{
|
||||
Addrs: strings.Split(url.Host, ","),
|
||||
Database: strings.TrimPrefix(url.Path, "/"),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
if url.User != nil {
|
||||
info.Username = url.User.Username()
|
||||
info.Password, _ = url.User.Password()
|
||||
}
|
||||
|
||||
query := url.Query()
|
||||
for key, values := range query {
|
||||
var value string
|
||||
if len(values) > 0 {
|
||||
value = values[0]
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "authSource":
|
||||
info.Source = value
|
||||
case "authMechanism":
|
||||
info.Mechanism = value
|
||||
case "gssapiServiceName":
|
||||
info.Service = value
|
||||
case "replicaSet":
|
||||
info.ReplicaSetName = value
|
||||
case "maxPoolSize":
|
||||
poolLimit, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("bad value for maxPoolSize: " + value)
|
||||
}
|
||||
info.PoolLimit = poolLimit
|
||||
case "ssl":
|
||||
// Unfortunately, mgo doesn't support the ssl parameter in its MongoDB URI parsing logic, so we have to handle that
|
||||
// ourselves. See https://github.com/go-mgo/mgo/issues/84
|
||||
ssl, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("bad value for ssl: " + value)
|
||||
}
|
||||
if ssl {
|
||||
info.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) {
|
||||
return tls.Dial("tcp", addr.String(), &tls.Config{})
|
||||
}
|
||||
}
|
||||
case "connect":
|
||||
if value == "direct" {
|
||||
info.Direct = true
|
||||
break
|
||||
}
|
||||
if value == "replicaSet" {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return nil, errors.New("unsupported connection URL option: " + key + "=" + value)
|
||||
}
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
36
plugins/database/mongodb/credentials_producer.go
Normal file
36
plugins/database/mongodb/credentials_producer.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
)
|
||||
|
||||
// mongoDBCredentialsProducer implements CredentialsProducer and provides an
|
||||
// interface for databases to generate user information.
|
||||
type mongoDBCredentialsProducer struct{}
|
||||
|
||||
func (cp *mongoDBCredentialsProducer) GenerateUsername(displayName string) (string, error) {
|
||||
userUUID, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
username := fmt.Sprintf("vault-%s-%s", displayName, userUUID)
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func (cp *mongoDBCredentialsProducer) GeneratePassword() (string, error) {
|
||||
password, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (cp *mongoDBCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
21
plugins/database/mongodb/mongodb-database-plugin/main.go
Normal file
21
plugins/database/mongodb/mongodb-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/mongodb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClientMeta := &pluginutil.APIClientMeta{}
|
||||
flags := apiClientMeta.FlagSet()
|
||||
flags.Parse(os.Args)
|
||||
|
||||
err := mongodb.Run(apiClientMeta.GetTLSConfig())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
168
plugins/database/mongodb/mongodb.go
Normal file
168
plugins/database/mongodb/mongodb.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
"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"
|
||||
"gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
const mongoDBTypeName = "mongodb"
|
||||
|
||||
// MongoDB is an implementation of Database interface
|
||||
type MongoDB struct {
|
||||
connutil.ConnectionProducer
|
||||
credsutil.CredentialsProducer
|
||||
}
|
||||
|
||||
// New returns a new MongoDB instance
|
||||
func New() (interface{}, error) {
|
||||
connProducer := &mongoDBConnectionProducer{}
|
||||
connProducer.Type = mongoDBTypeName
|
||||
|
||||
credsProducer := &mongoDBCredentialsProducer{}
|
||||
|
||||
dbType := &MongoDB{
|
||||
ConnectionProducer: connProducer,
|
||||
CredentialsProducer: credsProducer,
|
||||
}
|
||||
return dbType, nil
|
||||
}
|
||||
|
||||
// Run instantiates a MongoDB 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.(*MongoDB), apiTLSConfig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the TypeName for this backend
|
||||
func (m *MongoDB) Type() (string, error) {
|
||||
return mongoDBTypeName, nil
|
||||
}
|
||||
|
||||
func (m *MongoDB) getConnection() (*mgo.Session, error) {
|
||||
session, err := m.Connection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session.(*mgo.Session), nil
|
||||
}
|
||||
|
||||
// CreateUser generates the username/password on the underlying secret backend as instructed by
|
||||
// the CreationStatement provided. The creation statement is a JSON blob that has a db value,
|
||||
// and an array of roles that accepts a role, and an optional db value pair. This array will
|
||||
// be normalized the format specified in the mongoDB docs:
|
||||
// https://docs.mongodb.com/manual/reference/command/createUser/#dbcmd.createUser
|
||||
//
|
||||
// JSON Example:
|
||||
// { "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }
|
||||
func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) {
|
||||
// Grab the lock
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if statements.CreationStatements == "" {
|
||||
return "", "", dbutil.ErrEmptyCreationStatement
|
||||
}
|
||||
|
||||
session, err := m.getConnection()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
username, err = m.GenerateUsername(usernamePrefix)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
password, err = m.GeneratePassword()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Unmarshal statements.CreationStatements into mongodbRoles
|
||||
var mongoCS mongoDBStatement
|
||||
err = json.Unmarshal([]byte(statements.CreationStatements), &mongoCS)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Default to "admin" if no db provided
|
||||
if mongoCS.DB == "" {
|
||||
mongoCS.DB = "admin"
|
||||
}
|
||||
|
||||
if len(mongoCS.Roles) == 0 {
|
||||
return "", "", fmt.Errorf("roles array is required in creation statement")
|
||||
}
|
||||
|
||||
createUserCmd := createUserCommand{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Roles: mongoCS.Roles.toStandardRolesArray(),
|
||||
}
|
||||
|
||||
err = session.DB(mongoCS.DB).Run(createUserCmd, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
// RenewUser is not supported on MongoDB, so this is a no-op.
|
||||
func (m *MongoDB) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
|
||||
// NOOP
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeUser drops the specified user from the authentication databse. If none is provided
|
||||
// in the revocation statement, the default "admin" authentication database will be assumed.
|
||||
func (m *MongoDB) RevokeUser(statements dbplugin.Statements, username string) error {
|
||||
session, err := m.getConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If no revocation statements provided, pass in empty JSON
|
||||
revocationStatement := statements.RevocationStatements
|
||||
if revocationStatement == "" {
|
||||
revocationStatement = `{}`
|
||||
}
|
||||
|
||||
// Unmarshal revocation statements into mongodbRoles
|
||||
var mongoCS mongoDBStatement
|
||||
err = json.Unmarshal([]byte(revocationStatement), &mongoCS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db := mongoCS.DB
|
||||
// If db is not specified, use the default authenticationDatabase "admin"
|
||||
if db == "" {
|
||||
db = "admin"
|
||||
}
|
||||
|
||||
err = session.DB(db).RemoveUser(username)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
183
plugins/database/mongodb/mongodb_test.go
Normal file
183
plugins/database/mongodb/mongodb_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const testMongoDBRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }`
|
||||
|
||||
func prepareMongoDBTestContainer(t *testing.T) (cleanup func(), retURL string) {
|
||||
if os.Getenv("MONGODB_URL") != "" {
|
||||
return func() {}, os.Getenv("MONGODB_URL")
|
||||
}
|
||||
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to docker: %s", err)
|
||||
}
|
||||
|
||||
resource, err := pool.Run("mongo", "latest", []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start local mongo 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("mongodb://localhost:%s", resource.GetPort("27017/tcp"))
|
||||
|
||||
// exponential backoff-retry
|
||||
if err = pool.Retry(func() error {
|
||||
var err error
|
||||
dialInfo, err := parseMongoURL(retURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := mgo.DialWithInfo(dialInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.SetSyncTimeout(1 * time.Minute)
|
||||
session.SetSocketTimeout(1 * time.Minute)
|
||||
return session.Ping()
|
||||
}); err != nil {
|
||||
t.Fatalf("Could not connect to mongo docker container: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestMongoDB_Initialize(t *testing.T) {
|
||||
cleanup, connURL := prepareMongoDBTestContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": connURL,
|
||||
}
|
||||
|
||||
dbRaw, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
db := dbRaw.(*MongoDB)
|
||||
connProducer := db.ConnectionProducer.(*mongoDBConnectionProducer)
|
||||
|
||||
err = db.Initialize(connectionDetails, true)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !connProducer.Initialized {
|
||||
t.Fatal("Database should be initialized")
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMongoDB_CreateUser(t *testing.T) {
|
||||
cleanup, connURL := prepareMongoDBTestContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": connURL,
|
||||
}
|
||||
|
||||
dbRaw, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
db := dbRaw.(*MongoDB)
|
||||
err = db.Initialize(connectionDetails, true)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
statements := dbplugin.Statements{
|
||||
CreationStatements: testMongoDBRole,
|
||||
}
|
||||
|
||||
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 TestMongoDB_RevokeUser(t *testing.T) {
|
||||
cleanup, connURL := prepareMongoDBTestContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
connectionDetails := map[string]interface{}{
|
||||
"connection_url": connURL,
|
||||
}
|
||||
|
||||
dbRaw, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
db := dbRaw.(*MongoDB)
|
||||
err = db.Initialize(connectionDetails, true)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
statements := dbplugin.Statements{
|
||||
CreationStatements: testMongoDBRole,
|
||||
}
|
||||
|
||||
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 revocation statememt
|
||||
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 {
|
||||
connURL = strings.Replace(connURL, "mongodb://", fmt.Sprintf("mongodb://%s:%s@", username, password), 1)
|
||||
dialInfo, err := parseMongoURL(connURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := mgo.DialWithInfo(dialInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.SetSyncTimeout(1 * time.Minute)
|
||||
session.SetSocketTimeout(1 * time.Minute)
|
||||
return session.Ping()
|
||||
}
|
||||
39
plugins/database/mongodb/util.go
Normal file
39
plugins/database/mongodb/util.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package mongodb
|
||||
|
||||
type createUserCommand struct {
|
||||
Username string `bson:"createUser"`
|
||||
Password string `bson:"pwd"`
|
||||
Roles []interface{} `bson:"roles"`
|
||||
}
|
||||
type mongodbRole struct {
|
||||
Role string `json:"role" bson:"role"`
|
||||
DB string `json:"db" bson:"db"`
|
||||
}
|
||||
|
||||
type mongodbRoles []mongodbRole
|
||||
|
||||
type mongoDBStatement struct {
|
||||
DB string `json:"db"`
|
||||
Roles mongodbRoles `json:"roles"`
|
||||
}
|
||||
|
||||
// Convert array of role documents like:
|
||||
//
|
||||
// [ { "role": "readWrite" }, { "role": "readWrite", "db": "test" } ]
|
||||
//
|
||||
// into a "standard" MongoDB roles array containing both strings and role documents:
|
||||
//
|
||||
// [ "readWrite", { "role": "readWrite", "db": "test" } ]
|
||||
//
|
||||
// MongoDB's createUser command accepts the latter.
|
||||
func (roles mongodbRoles) toStandardRolesArray() []interface{} {
|
||||
var standardRolesArray []interface{}
|
||||
for _, role := range roles {
|
||||
if role.DB == "" {
|
||||
standardRolesArray = append(standardRolesArray, role.Role)
|
||||
} else {
|
||||
standardRolesArray = append(standardRolesArray, role)
|
||||
}
|
||||
}
|
||||
return standardRolesArray
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errNotInitialized = errors.New("connection has not been initalized")
|
||||
ErrNotInitialized = errors.New("connection has not been initalized")
|
||||
)
|
||||
|
||||
// ConnectionProducer can be used as an embeded interface in the Database
|
||||
|
||||
@@ -61,22 +61,28 @@ func (c *SQLConnectionProducer) Initialize(conf map[string]interface{}, verifyCo
|
||||
return fmt.Errorf("invalid max_connection_lifetime: %s", err)
|
||||
}
|
||||
|
||||
// Set initialized to true at this point since all fields are set,
|
||||
// and the connection can be established at a later time.
|
||||
c.Initialized = true
|
||||
|
||||
if verifyConnection {
|
||||
if _, err := c.Connection(); err != nil {
|
||||
return fmt.Errorf("error initalizing connection: %s", err)
|
||||
return fmt.Errorf("error verifying connection: %s", err)
|
||||
}
|
||||
|
||||
if err := c.db.Ping(); err != nil {
|
||||
return fmt.Errorf("error initalizing connection: %s", err)
|
||||
return fmt.Errorf("error verifying connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Initialized = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SQLConnectionProducer) Connection() (interface{}, error) {
|
||||
if !c.Initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
|
||||
// If we already have a DB, test it and return
|
||||
if c.db != nil {
|
||||
if err := c.db.Ping(); err == nil {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: "api"
|
||||
page_title: "Cassandra Database Plugin - HTTP API"
|
||||
sidebar_current: "docs-http-secret-databases-cassandra-maria"
|
||||
sidebar_current: "docs-http-secret-databases-cassandra"
|
||||
description: |-
|
||||
The Cassandra plugin for Vault's Database backend generates database credentials to access Cassandra servers.
|
||||
---
|
||||
|
||||
87
website/source/api/secret/databases/mongodb.html.md
Normal file
87
website/source/api/secret/databases/mongodb.html.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
layout: "api"
|
||||
page_title: "MongoDB Database Plugin - HTTP API"
|
||||
sidebar_current: "docs-http-secret-databases-mongodb"
|
||||
description: |-
|
||||
The MongoDB plugin for Vault's Database backend generates database credentials to access MongoDB servers.
|
||||
---
|
||||
|
||||
# MongoDB Database Plugin HTTP API
|
||||
|
||||
The MongoDB Database Plugin is one of the supported plugins for the Database
|
||||
backend. This plugin generates database credentials dynamically based on
|
||||
configured roles for the MongoDB 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 MongoDB standard connection string (URI).
|
||||
|
||||
### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin_name": "mongodb-database-plugin",
|
||||
"allowed_roles": "readonly",
|
||||
"connection_url": "mongodb://admin:Password!@mongodb.acme.com:27017/admin?ssl=true"
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
https://vault.rocks/v1/database/config/mongodb
|
||||
```
|
||||
|
||||
## Statements
|
||||
|
||||
Statements are configured during role creation and are used by the plugin to
|
||||
determine what is sent to the datatabse on user creation, renewing, and
|
||||
revocation. For more information on configuring roles see the [Role
|
||||
API](/api/secret/databases/index.html#create-role) in the Database Backend docs.
|
||||
|
||||
### Parameters
|
||||
|
||||
The following are the statements used by this plugin. If not mentioned in this
|
||||
list the plugin does not support that statement type.
|
||||
|
||||
- `creation_statements` `(string: <required>)` – Specifies the database
|
||||
statements executed to create and configure a user. Must be a
|
||||
serialized JSON object, or a base64-encoded serialized JSON object.
|
||||
The object can optionally contain a "db" string for session connection,
|
||||
and must contain a "roles" array. This array contains objects that holds
|
||||
a "role", and an optional "db" value, and is similar to the BSON document that
|
||||
is accepted by MongoDB's `roles` field. Vault will transform this array into
|
||||
such format. For more information regarding the `roles` field, refer to
|
||||
[MongoDB's documentation](https://docs.mongodb.com/manual/reference/method/db.createUser/).
|
||||
|
||||
- `revocation_statements` `(string: "")` – Specifies the database statements to
|
||||
be executed to revoke a user. Must be a serialized JSON object, or a base64-encoded
|
||||
serialized JSON object. The object can optionally contain a "db" string. If no
|
||||
"db" value is provided, it defaults to the "admin" database.
|
||||
|
||||
### Sample Creation Statement
|
||||
|
||||
```json
|
||||
{
|
||||
"db": "admin",
|
||||
"roles": [
|
||||
{
|
||||
"role": "read",
|
||||
"db": "foo",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: "api"
|
||||
page_title: "MSSQL Database Plugin - HTTP API"
|
||||
sidebar_current: "docs-http-secret-databases-mssql-maria"
|
||||
sidebar_current: "docs-http-secret-databases-mssql"
|
||||
description: |-
|
||||
The MSSQL plugin for Vault's Database backend generates database credentials to access MSSQL servers.
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: "api"
|
||||
page_title: "PostgreSQL Database Plugin - HTTP API"
|
||||
sidebar_current: "docs-http-secret-databases-postgresql-maria"
|
||||
sidebar_current: "docs-http-secret-databases-postgresql"
|
||||
description: |-
|
||||
The PostgreSQL plugin for Vault's Database backend generates database credentials to access PostgreSQL servers.
|
||||
---
|
||||
|
||||
58
website/source/docs/secrets/databases/mongodb.html.md
Normal file
58
website/source/docs/secrets/databases/mongodb.html.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "MongoDB Database Plugin"
|
||||
sidebar_current: "docs-secrets-databases-mongodb"
|
||||
description: |-
|
||||
The MongoDB plugin for Vault's Database backend generates database credentials to access MongoDB.
|
||||
---
|
||||
|
||||
# MongoDB Database Plugin
|
||||
|
||||
Name: `mongodb-database-plugin`
|
||||
|
||||
The MongoDB Database Plugin is one of the supported plugins for the Database
|
||||
backend. This plugin generates database credentials dynamically based on
|
||||
configured roles for the MongoDB 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 MongoDB connection
|
||||
by specifying this plugin as the `"plugin_name"` argument. Here is an example
|
||||
MongoDB configuration:
|
||||
|
||||
```
|
||||
$ vault write database/config/mongodb \
|
||||
plugin_name=mongodb-database-plugin \
|
||||
allowed_roles="readonly" \
|
||||
connection_url="mongodb://admin:Password!@mongodb.acme.com:27017/admin?ssl=true"
|
||||
|
||||
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 MongoDB connection is configured we can add a role:
|
||||
|
||||
```
|
||||
$ vault write database/roles/readonly \
|
||||
db_name=mongodb \
|
||||
creation_statements='{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }' \
|
||||
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 [MongoDB database
|
||||
plugin API](/api/secret/databases/mongodb.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.
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<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-mongodb") %>>
|
||||
<a href="/api/secret/databases/mongodb.html">MongoDB</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-http-secret-databases-mssql") %>>
|
||||
<a href="/api/secret/databases/mssql.html">MSSQL</a>
|
||||
</li>
|
||||
@@ -52,7 +55,7 @@
|
||||
<a href="/api/secret/generic/index.html">Generic</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-http-secret-mongodb") %>>
|
||||
<a href="/api/secret/mongodb/index.html">MongoDB</a>
|
||||
<a href="/api/secret/mongodb/index.html">MongoDB (Deprecated)</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-http-secret-mssql") %>>
|
||||
<a href="/api/secret/mssql/index.html">MSSQL (Deprecated)</a>
|
||||
|
||||
@@ -225,6 +225,9 @@
|
||||
<li<%= sidebar_current("docs-secrets-databases-cassandra") %>>
|
||||
<a href="/docs/secrets/databases/cassandra.html">Cassandra</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-secrets-databases-mongodb") %>>
|
||||
<a href="/docs/secrets/databases/mysql-maria.html">MongoDB</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-secrets-databases-mssql") %>>
|
||||
<a href="/docs/secrets/databases/mssql.html">MSSQL</a>
|
||||
</li>
|
||||
@@ -245,7 +248,7 @@
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-secrets-mongodb") %>>
|
||||
<a href="/docs/secrets/mongodb/index.html">MongoDB</a>
|
||||
<a href="/docs/secrets/mongodb/index.html">MongoDB (Deprecated)</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-secrets-mssql") %>>
|
||||
|
||||
Reference in New Issue
Block a user