diff --git a/plugins/database/cassandra/cassandra.go b/plugins/database/cassandra/cassandra.go index 60e445ff6bae..cafb2545cfb1 100644 --- a/plugins/database/cassandra/cassandra.go +++ b/plugins/database/cassandra/cassandra.go @@ -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, diff --git a/plugins/database/cassandra/cassandra_test.go b/plugins/database/cassandra/cassandra_test.go index 9e98ec48f558..eaa5dacd9d31 100644 --- a/plugins/database/cassandra/cassandra_test.go +++ b/plugins/database/cassandra/cassandra_test.go @@ -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" ) @@ -82,7 +81,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 { diff --git a/plugins/helper/database/connutil/cassandra.go b/plugins/database/cassandra/connection_producer.go similarity index 90% rename from plugins/helper/database/connutil/cassandra.go rename to plugins/database/cassandra/connection_producer.go index 869c39e3b6dc..0b484d1c993f 100644 --- a/plugins/helper/database/connutil/cassandra.go +++ b/plugins/database/cassandra/connection_producer.go @@ -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, diff --git a/plugins/helper/database/credsutil/cassandra.go b/plugins/database/cassandra/credentials_producer.go similarity index 65% rename from plugins/helper/database/credsutil/cassandra.go rename to plugins/database/cassandra/credentials_producer.go index 7ab5630b5809..8e9c3a509adf 100644 --- a/plugins/helper/database/credsutil/cassandra.go +++ b/plugins/database/cassandra/credentials_producer.go @@ -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 } diff --git a/plugins/database/cassandra/test-fixtures/cassandra.yaml b/plugins/database/cassandra/test-fixtures/cassandra.yaml index 54f47d34ac62..a6ef938c4131 100644 --- a/plugins/database/cassandra/test-fixtures/cassandra.yaml +++ b/plugins/database/cassandra/test-fixtures/cassandra.yaml @@ -421,7 +421,7 @@ seed_provider: parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - - 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 diff --git a/plugins/database/mongodb/connection_producer.go b/plugins/database/mongodb/connection_producer.go new file mode 100644 index 000000000000..5fcfdecbdf2b --- /dev/null +++ b/plugins/database/mongodb/connection_producer.go @@ -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 +} diff --git a/plugins/database/mongodb/credentials_producer.go b/plugins/database/mongodb/credentials_producer.go new file mode 100644 index 000000000000..80dc2c3d39d1 --- /dev/null +++ b/plugins/database/mongodb/credentials_producer.go @@ -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 +} diff --git a/plugins/database/mongodb/mongodb-database-plugin/main.go b/plugins/database/mongodb/mongodb-database-plugin/main.go new file mode 100644 index 000000000000..30db69a12e94 --- /dev/null +++ b/plugins/database/mongodb/mongodb-database-plugin/main.go @@ -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) + } +} diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go new file mode 100644 index 000000000000..5d7aa09b1c71 --- /dev/null +++ b/plugins/database/mongodb/mongodb.go @@ -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 +} diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go new file mode 100644 index 000000000000..1fa14aa37fc4 --- /dev/null +++ b/plugins/database/mongodb/mongodb_test.go @@ -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() +} diff --git a/plugins/database/mongodb/util.go b/plugins/database/mongodb/util.go new file mode 100644 index 000000000000..9004a3c710c4 --- /dev/null +++ b/plugins/database/mongodb/util.go @@ -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 +} diff --git a/plugins/helper/database/connutil/connutil.go b/plugins/helper/database/connutil/connutil.go index c43691c6164d..d36d5719d6a8 100644 --- a/plugins/helper/database/connutil/connutil.go +++ b/plugins/helper/database/connutil/connutil.go @@ -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 diff --git a/plugins/helper/database/connutil/sql.go b/plugins/helper/database/connutil/sql.go index 04269798f190..4cb99744298f 100644 --- a/plugins/helper/database/connutil/sql.go +++ b/plugins/helper/database/connutil/sql.go @@ -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 { diff --git a/website/source/api/secret/databases/cassandra.html.md b/website/source/api/secret/databases/cassandra.html.md index 5e2b5a83603b..72a2e18845f7 100644 --- a/website/source/api/secret/databases/cassandra.html.md +++ b/website/source/api/secret/databases/cassandra.html.md @@ -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. --- diff --git a/website/source/api/secret/databases/mongodb.html.md b/website/source/api/secret/databases/mongodb.html.md new file mode 100644 index 000000000000..48a8ae2c401d --- /dev/null +++ b/website/source/api/secret/databases/mongodb.html.md @@ -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: )` – 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: )` – 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", + } + ] +} +``` \ No newline at end of file diff --git a/website/source/api/secret/databases/mssql.html.md b/website/source/api/secret/databases/mssql.html.md index d4b120e8d62c..75b44b60d205 100644 --- a/website/source/api/secret/databases/mssql.html.md +++ b/website/source/api/secret/databases/mssql.html.md @@ -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. --- diff --git a/website/source/api/secret/databases/postgresql.html.md b/website/source/api/secret/databases/postgresql.html.md index a1aaeee1c65a..27844bda96cc 100644 --- a/website/source/api/secret/databases/postgresql.html.md +++ b/website/source/api/secret/databases/postgresql.html.md @@ -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. --- diff --git a/website/source/docs/secrets/databases/mongodb.html.md b/website/source/docs/secrets/databases/mongodb.html.md new file mode 100644 index 000000000000..d285c50620d0 --- /dev/null +++ b/website/source/docs/secrets/databases/mongodb.html.md @@ -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. + diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index 799e461d0c2f..016aa4265c4a 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -36,6 +36,9 @@ > Cassandra + > + MongoDB + > MSSQL @@ -52,7 +55,7 @@ Generic > - MongoDB + MongoDB (Deprecated) > MSSQL (Deprecated) diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index ac6d96cbb80c..5c8a8bac69bc 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -225,6 +225,9 @@ > Cassandra + > + MongoDB + > MSSQL @@ -245,7 +248,7 @@ > - MongoDB + MongoDB (Deprecated) >