Add mongodbatlas static roles support (#8987)

* Refactor PG container creation.
* Rework rotation tests to use shorter sleeps.
* Refactor rotation tests.
* Add a static role rotation test for MongoDB Atlas.
This commit is contained in:
ncabatoff
2020-05-29 14:21:23 -04:00
committed by GitHub
parent 7561c04921
commit 44fdbc7dc9
15 changed files with 262 additions and 523 deletions

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/go-test/deep"
"github.com/hashicorp/vault-plugin-database-mongodbatlas"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers/docker"
@@ -104,6 +105,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) {
sys := vault.TestDynamicSystemView(cores[0].Core)
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "")
vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Mongo", []string{}, "")
vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlas", []string{}, "")
return cluster, sys
}
@@ -149,6 +151,28 @@ func TestBackend_PluginMain_Mongo(t *testing.T) {
}
}
func TestBackend_PluginMain_MongoAtlas(t *testing.T) {
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
return
}
caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv)
if caPEM == "" {
t.Fatal("CA cert not passed in")
}
args := []string{"--ca-cert=" + caPEM}
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(args)
err := mongodbatlas.Run(apiClientMeta.GetTLSConfig())
if err != nil {
t.Fatal(err)
}
}
func TestBackend_RoleUpgrade(t *testing.T) {
storage := &logical.InmemStorage{}

View File

@@ -3,18 +3,22 @@ package database
import (
"context"
"log"
"os"
"strings"
"testing"
"time"
"database/sql"
"github.com/Sectorbob/mlab-ns2/gae/ns/digest"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers/mongodb"
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/hashicorp/vault/sdk/logical"
"github.com/lib/pq"
mongodbatlasapi "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@@ -44,7 +48,7 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// create the database user
@@ -192,7 +196,7 @@ func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// create the database user
@@ -296,7 +300,7 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// create the database user
@@ -521,7 +525,7 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
t.Fatal("could not convert to db backend")
}
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// create the database user
@@ -685,143 +689,83 @@ func assertWALCount(t *testing.T, s logical.Storage, expected int, key string) {
// End WAL testing
//
type userCreator func(t *testing.T, username, password string)
func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(context.Background())
bd := b.(*databaseBackend)
if bd.credRotationQueue == nil {
t.Fatal("database backend had no credential rotation queue")
}
// Configure backend, add item and confirm length
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest")
defer cleanup()
testCases := []string{"65", "130", "5400"}
// Create database users ahead
for _, tc := range testCases {
createTestPGUser(t, connURL, dbUser+tc, dbUserDefaultPassword, testRoleStaticCreate)
}
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Create three static roles with different rotation periods
for _, tc := range testCases {
roleName := "plugin-static-role-" + tc
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"rotation_statements": testRoleStaticUpdate,
"username": dbUser + tc,
"rotation_period": tc,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
}
// Verify the queue has 3 items in it
if bd.credRotationQueue.Len() != 3 {
t.Fatalf("expected 3 items in the rotation queue, got: (%d)", bd.credRotationQueue.Len())
}
// List the roles
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ListOperation,
Path: "static-roles/",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 3 {
t.Fatalf("expected 3 roles, got: (%d)", len(keys))
}
// Capture initial passwords, before the periodic function is triggered
pws := make(map[string][]string, 0)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep to make sure the 65s role will be up for rotation by the time the
// periodic function ticks
time.Sleep(7 * time.Second)
// Sleep 75 to make sure the periodic func has time to actually run
time.Sleep(75 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep more, this should allow both sr65 and sr130 to rotate
time.Sleep(140 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Verify all pws are as they should
pass := true
for k, v := range pws {
switch {
case k == "plugin-static-role-65":
// expect all passwords to be different
if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-130":
// expect the first two to be equal, but different from the third
if v[0] != v[1] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-5400":
// expect all passwords to be equal
if v[0] != v[1] || v[1] != v[2] {
pass = false
}
}
}
if !pass {
t.Fatalf("password rotations did not match expected: %#v", pws)
}
uc := userCreator(func(t *testing.T, username, password string) {
createTestPGUser(t, connURL, username, password, testRoleStaticCreate)
})
testBackend_StaticRole_Rotations(t, uc, map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
})
}
func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "latest", "vaulttestdb")
defer cleanup()
uc := userCreator(func(t *testing.T, username, password string) {
testCreateDBUser(t, connURL, "vaulttestdb", username, password)
})
testBackend_StaticRole_Rotations(t, uc, map[string]interface{}{
"connection_url": connURL,
"plugin_name": "mongodb-database-plugin",
})
}
func TestBackend_StaticRole_Rotations_MongoDBAtlas(t *testing.T) {
// To get the project ID, connect to cloud.mongodb.com, go to the vault-test project and
// look at Project Settings.
projID := os.Getenv("VAULT_MONGODBATLAS_PROJECT_ID")
// For the private and public key, go to Organization Access Manager on cloud.mongodb.com,
// choose Create API Key, then create one using the defaults. Then go back to the vault-test
// project and add the API key to it, with permissions "Project Owner".
privKey := os.Getenv("VAULT_MONGODBATLAS_PRIVATE_KEY")
pubKey := os.Getenv("VAULT_MONGODBATLAS_PUBLIC_KEY")
if projID == "" {
t.Logf("Skipping MongoDB Atlas test because VAULT_MONGODBATLAS_PROJECT_ID not set")
t.SkipNow()
}
transport := digest.NewTransport(pubKey, privKey)
cl, err := transport.Client()
if err != nil {
t.Fatal(err)
}
api, err := mongodbatlasapi.New(cl)
if err != nil {
t.Fatal(err)
}
uc := userCreator(func(t *testing.T, username, password string) {
// Delete the user in case it's still there from an earlier run, ignore
// errors in case it's not.
_, _ = api.DatabaseUsers.Delete(context.Background(), projID, username)
req := &mongodbatlasapi.DatabaseUser{
Username: username,
Password: password,
DatabaseName: "admin",
Roles: []mongodbatlasapi.Role{{RoleName: "atlasAdmin", DatabaseName: "admin"}},
}
_, _, err := api.DatabaseUsers.Create(context.Background(), projID, req)
if err != nil {
t.Fatal(err)
}
})
testBackend_StaticRole_Rotations(t, uc, map[string]interface{}{
"plugin_name": "mongodbatlas-database-plugin",
"project_id": projID,
"private_key": privKey,
"public_key": pubKey,
})
}
func testBackend_StaticRole_Rotations(t *testing.T, createUser userCreator, opts map[string]interface{}) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
@@ -829,6 +773,7 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
config.StorageView = &logical.InmemStorage{}
config.System = sys
// Rotation ticker starts running in Factory call
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
@@ -841,27 +786,18 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
t.Fatal("database backend had no credential rotation queue")
}
// configure backend, add item and confirm length
cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "latest", "vaulttestdb")
defer cleanup()
testCases := []string{"65", "130", "5400"}
// Create database users ahead
for _, tc := range testCases {
testCreateDBUser(t, connURL, "vaulttestdb", "statictestMongo"+tc, "test")
}
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "mongodb-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-mongo-test",
}
for k, v := range opts {
data[k] = v
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-mongo-test",
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
@@ -870,13 +806,19 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
testCases := []string{"10", "20", "100"}
// Create database users ahead
for _, tc := range testCases {
createUser(t, "statictest"+tc, "test")
}
// create three static roles with different rotation periods
for _, tc := range testCases {
roleName := "plugin-static-role-" + tc
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-mongo-test",
"username": "statictestMongo" + tc,
"db_name": "plugin-test",
"username": "statictest" + tc,
"rotation_period": tc,
}
@@ -920,16 +862,12 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
pws := make(map[string][]string, 0)
pws = capturePasswords(t, b, config, testCases, pws)
// sleep to make sure the 65s role will be up for rotation by the time the
// periodic function ticks
time.Sleep(7 * time.Second)
// sleep 75 to make sure the periodic func has time to actually run
time.Sleep(75 * time.Second)
// sleep to make sure the periodic func has time to actually run
time.Sleep(15 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// sleep more, this should allow both sr65 and sr130 to rotate
time.Sleep(140 * time.Second)
// sleep more, this should allow both sr10 and sr20 to rotate
time.Sleep(10 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// verify all pws are as they should
@@ -939,21 +877,23 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
t.Fatalf("expected to find 3 passwords for (%s), only found (%d)", k, len(v))
}
switch {
case k == "plugin-static-role-65":
case k == "plugin-static-role-10":
// expect all passwords to be different
if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-130":
case k == "plugin-static-role-20":
// expect the first two to be equal, but different from the third
if v[0] != v[1] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-5400":
case k == "plugin-static-role-100":
// expect all passwords to be equal
if v[0] != v[1] || v[1] != v[2] {
pass = false
}
default:
t.Fatalf("unexpected password key: %v", k)
}
}
if !pass {
@@ -1004,7 +944,7 @@ func TestBackend_StaticRole_LockRegression(t *testing.T) {
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// Configure a connection
@@ -1073,7 +1013,7 @@ func TestBackend_StaticRole_Rotate_Invalid_Role(t *testing.T) {
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
// create the database user

View File

@@ -6,58 +6,17 @@ import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"reflect"
"testing"
"github.com/hashicorp/vault/helper/testhelpers/docker"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
"github.com/hashicorp/vault/sdk/logical"
"github.com/lib/pq"
"github.com/mitchellh/mapstructure"
"github.com/ory/dockertest"
)
func prepareTestContainer(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() {
docker.CleanupResource(t, pool, resource)
}
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
}
defer db.Close()
return db.Ping()
}); err != nil {
cleanup()
t.Fatalf("Could not connect to PostgreSQL docker container: %s", err)
}
return
}
func TestBackend_config_connection(t *testing.T) {
var resp *logical.Response
var err error
@@ -107,7 +66,7 @@ func TestBackend_basic(t *testing.T) {
t.Fatal(err)
}
cleanup, connURL := prepareTestContainer(t)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
connData := map[string]interface{}{
@@ -131,7 +90,7 @@ func TestBackend_roleCrud(t *testing.T) {
t.Fatal(err)
}
cleanup, connURL := prepareTestContainer(t)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
connData := map[string]interface{}{
@@ -157,7 +116,7 @@ func TestBackend_BlockStatements(t *testing.T) {
t.Fatal(err)
}
cleanup, connURL := prepareTestContainer(t)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
connData := map[string]interface{}{
@@ -187,7 +146,7 @@ func TestBackend_roleReadOnly(t *testing.T) {
t.Fatal(err)
}
cleanup, connURL := prepareTestContainer(t)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
connData := map[string]interface{}{
@@ -218,7 +177,7 @@ func TestBackend_roleReadOnly_revocationSQL(t *testing.T) {
t.Fatal(err)
}
cleanup, connURL := prepareTestContainer(t)
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
connData := map[string]interface{}{

4
go.mod
View File

@@ -15,6 +15,7 @@ require (
github.com/DataDog/zstd v1.4.4 // indirect
github.com/NYTimes/gziphandler v1.1.1
github.com/SAP/go-hdb v0.14.1
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5
@@ -81,7 +82,7 @@ require (
github.com/hashicorp/vault-plugin-auth-kubernetes v0.6.1
github.com/hashicorp/vault-plugin-auth-oci v0.5.4
github.com/hashicorp/vault-plugin-database-elasticsearch v0.5.4
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.1
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.0-beta1.0.20200521152755-9cf156a44f9c
github.com/hashicorp/vault-plugin-secrets-ad v0.6.4-beta1.0.20200518124111-3dceeb3ce90e
github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.5
github.com/hashicorp/vault-plugin-secrets-azure v0.5.6
@@ -111,6 +112,7 @@ require (
github.com/mitchellh/gox v1.0.1
github.com/mitchellh/mapstructure v1.2.2
github.com/mitchellh/reflectwalk v1.0.1
github.com/mongodb/go-client-mongodb-atlas v0.1.2
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc
github.com/ncw/swift v1.0.47
github.com/nwaples/rardecode v1.0.0 // indirect

2
go.sum
View File

@@ -496,6 +496,8 @@ github.com/hashicorp/vault-plugin-auth-oci v0.5.4 h1:Hoauxh1V8Lusf7BRs+yXfoDTFQz
github.com/hashicorp/vault-plugin-auth-oci v0.5.4/go.mod h1:j05O2b9fw2Q82NxDPhHMYVfHKvitUYGWfmqmpBdqmmc=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.5.4 h1:YE4qndazWmYGpVOoZI7nDGG+gwTZKzL1Ou4WZQ+Tdxk=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.5.4/go.mod h1:QjGrrxcRXv/4XkEZAlM0VMZEa3uxKAICFqDj27FP/48=
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.0-beta1.0.20200521152755-9cf156a44f9c h1:9pXwe7sEVhZ5C3U6egIrKaZBb5lD0FvLIjISEvpbQQA=
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.0-beta1.0.20200521152755-9cf156a44f9c/go.mod h1:HTXNzFr/SAVtJOs7jz0XxZ69jlKtaceEwp37l86UAQ0=
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.1 h1:fA6cFH8lIPH2M4KNTEzf1bpc6Tbyy5ZvoYP8H/TI9ts=
github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.1/go.mod h1:MP3kfr0N+7miOTZFwKv952b9VkXM4S2Q6YtQCiNKWq8=
github.com/hashicorp/vault-plugin-secrets-ad v0.6.4-beta1.0.20200518124111-3dceeb3ce90e h1:0GK1BNBfglD2sydZ4XXMjJElhY8bC2TDdc0vk1Q9zbA=

View File

@@ -0,0 +1,53 @@
package postgresql
import (
"database/sql"
"fmt"
"os"
"testing"
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/ory/dockertest"
)
func PrepareTestContainer(t *testing.T, version string) (cleanup func(), retURL string) {
if os.Getenv("PG_URL") != "" {
return func() {}, os.Getenv("PG_URL")
}
if version == "" {
version = "latest"
}
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() {
docker.CleanupResource(t, pool, resource)
}
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
}
defer db.Close()
return db.Ping()
}); err != nil {
cleanup()
t.Fatalf("Could not connect to PostgreSQL docker container: %s", err)
}
return
}

View File

@@ -7,10 +7,9 @@ import (
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/helper/testhelpers/postgresql"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/physical"
"github.com/ory/dockertest"
_ "github.com/lib/pq"
)
@@ -19,11 +18,11 @@ func TestPostgreSQLBackend(t *testing.T) {
logger := logging.NewVaultLogger(log.Debug)
// Use docker as pg backend if no url is provided via environment variables
var cleanup func()
connURL := os.Getenv("PGURL")
if connURL == "" {
cleanup, connURL = prepareTestContainer(t, logger)
cleanup, u := postgresql.PrepareTestContainer(t, "11.1")
defer cleanup()
connURL = u
}
table := os.Getenv("PGTABLE")
@@ -361,47 +360,6 @@ func testPostgresSQLLockRenewal(t *testing.T, ha physical.HABackend) {
newLock.Unlock()
}
func prepareTestContainer(t *testing.T, logger log.Logger) (cleanup func(), retConnString string) {
// If environment variable is set, use this connectionstring without starting docker container
if os.Getenv("PGURL") != "" {
return func() {}, os.Getenv("PGURL")
}
pool, err := dockertest.NewPool("")
if err != nil {
t.Fatalf("Failed to connect to docker: %s", err)
}
// using 11.1 which is currently latest, use hard version for stability of tests
resource, err := pool.Run("postgres", "11.1", []string{})
if err != nil {
t.Fatalf("Could not start docker Postgres: %s", err)
}
retConnString = fmt.Sprintf("postgres://postgres@localhost:%v/postgres?sslmode=disable", resource.GetPort("5432/tcp"))
cleanup = func() {
docker.CleanupResource(t, pool, resource)
}
// Provide a test function to the pool to test if docker instance service is up.
// We try to setup a pg backend as test for successful connect
// exponential backoff-retry, because the dockerinstance may not be able to accept
// connections yet, test by trying to setup a postgres backend, max-timeout is 60s
if err := pool.Retry(func() error {
var err error
_, err = NewPostgreSQLBackend(map[string]string{
"connection_url": retConnString,
}, logger)
return err
}); err != nil {
cleanup()
t.Fatalf("Could not connect to docker: %s", err)
}
return cleanup, retConnString
}
func setupDatabaseObjects(t *testing.T, logger log.Logger, pg *PostgreSQLBackend) {
var err error
// Setup tables and indexes if not exists.

View File

@@ -4,64 +4,24 @@ import (
"context"
"database/sql"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/helper/testhelpers/postgresql"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/lib/pq"
"github.com/ory/dockertest"
)
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() {
docker.CleanupResource(t, pool, resource)
}
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
}
defer db.Close()
return db.Ping()
}); err != nil {
cleanup()
t.Fatalf("Could not connect to PostgreSQL docker container: %s", err)
}
return
}
func TestPostgreSQL_Initialize(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t)
defer cleanup()
func getPostgreSQL(t *testing.T, options map[string]interface{}) (*PostgreSQL, func()) {
cleanup, connURL := postgresql.PrepareTestContainer(t, "latest")
connectionDetails := map[string]interface{}{
"connection_url": connURL,
"max_open_connections": 5,
"connection_url": connURL,
}
for k, v := range options {
connectionDetails[k] = v
}
db := new()
@@ -73,23 +33,29 @@ func TestPostgreSQL_Initialize(t *testing.T) {
if !db.Initialized {
t.Fatal("Database should be initialized")
}
return db, cleanup
}
err = db.Close()
if err != nil {
func TestPostgreSQL_Initialize(t *testing.T) {
db, cleanup := getPostgreSQL(t, map[string]interface{}{
"max_open_connections": 5,
})
defer cleanup()
if err := db.Close(); err != nil {
t.Fatalf("err: %s", err)
}
}
// Test decoding a string value for max_open_connections
connectionDetails = map[string]interface{}{
"connection_url": connURL,
func TestPostgreSQL_InitializeWithStringVals(t *testing.T) {
db, cleanup := getPostgreSQL(t, map[string]interface{}{
"max_open_connections": "5",
}
})
defer cleanup()
_, err = db.Init(context.Background(), connectionDetails, true)
if err != nil {
if err := db.Close(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestPostgreSQL_CreateUser_missingArgs(t *testing.T) {
@@ -180,19 +146,9 @@ func TestPostgreSQL_CreateUser(t *testing.T) {
}
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := preparePostgresTestContainer(t)
db, cleanup := getPostgreSQL(t, nil)
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
db := new()
_, err := db.Init(context.Background(), connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
usernameConfig := dbplugin.UsernameConfig{
@@ -218,14 +174,14 @@ func TestPostgreSQL_CreateUser(t *testing.T) {
return
}
if err = testCredsExist(t, connURL, username, password); err != nil {
if err = testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
// Ensure that the role doesn't expire immediately
time.Sleep(2 * time.Second)
if err = testCredsExist(t, connURL, username, password); err != nil {
if err = testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
})
@@ -252,24 +208,9 @@ func TestPostgreSQL_RenewUser(t *testing.T) {
}
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := preparePostgresTestContainer(t)
db, cleanup := getPostgreSQL(t, nil)
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
db := new()
// Give a timeout just in case the test decides to be problematic
initCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := db.Init(initCtx, connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
statements := dbplugin.Statements{
@@ -291,7 +232,7 @@ func TestPostgreSQL_RenewUser(t *testing.T) {
t.Fatalf("err: %s", err)
}
if err = testCredsExist(t, connURL, username, password); err != nil {
if err = testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
@@ -303,7 +244,7 @@ func TestPostgreSQL_RenewUser(t *testing.T) {
// Sleep longer than the initial expiration time
time.Sleep(2 * time.Second)
if err = testCredsExist(t, connURL, username, password); err != nil {
if err = testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
})
@@ -331,9 +272,8 @@ func TestPostgreSQL_RotateRootCredentials(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t)
cleanup, connURL := postgresql.PrepareTestContainer(t, "latest")
defer cleanup()
connURL = strings.Replace(connURL, "postgres:secret", `{{username}}:{{password}}`, -1)
connectionDetails := map[string]interface{}{
@@ -344,7 +284,6 @@ func TestPostgreSQL_RotateRootCredentials(t *testing.T) {
}
db := new()
connProducer := db.SQLConnectionProducer
// Give a timeout just in case the test decides to be problematic
@@ -400,24 +339,9 @@ func TestPostgreSQL_RevokeUser(t *testing.T) {
}
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := preparePostgresTestContainer(t)
db, cleanup := getPostgreSQL(t, nil)
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
db := new()
// Give a timeout just in case the test decides to be problematic
initCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := db.Init(initCtx, connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
statements := dbplugin.Statements{
@@ -435,7 +359,7 @@ func TestPostgreSQL_RevokeUser(t *testing.T) {
t.Fatalf("err: %s", err)
}
if err = testCredsExist(t, connURL, username, password); err != nil {
if err = testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
@@ -445,7 +369,7 @@ func TestPostgreSQL_RevokeUser(t *testing.T) {
t.Fatalf("err: %s", err)
}
if err := testCredsExist(t, connURL, username, password); err == nil {
if err := testCredsExist(t, db.ConnectionURL, username, password); err == nil {
t.Fatal("Credentials were not revoked")
}
})
@@ -530,29 +454,13 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := preparePostgresTestContainer(t)
db, cleanup := getPostgreSQL(t, nil)
defer cleanup()
// create the database user
dbUser := "vaultstatictest"
initPassword := "password"
createTestPGUser(t, connURL, dbUser, initPassword, testRoleStaticCreate)
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
db := new()
// Give a timeout just in case the test decides to be problematic
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := db.Init(ctx, connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
createTestPGUser(t, db.ConnectionURL, dbUser, initPassword, testRoleStaticCreate)
statements := dbplugin.Statements{
Rotation: test.rotationStmts,
@@ -568,20 +476,20 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
Password: password,
}
if err := testCredsExist(t, connURL, dbUser, initPassword); err != nil {
if err := testCredsExist(t, db.ConnectionURL, dbUser, initPassword); err != nil {
t.Fatalf("Could not connect with initial credentials: %s", err)
}
username, password, err := db.SetCredentials(ctx, statements, usernameConfig)
username, password, err := db.SetCredentials(context.Background(), statements, usernameConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if err := testCredsExist(t, connURL, username, password); err != nil {
if err := testCredsExist(t, db.ConnectionURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
if err := testCredsExist(t, connURL, username, initPassword); err == nil {
if err := testCredsExist(t, db.ConnectionURL, username, initPassword); err == nil {
t.Fatalf("Should not be able to connect with initial credentials")
}
})

View File

@@ -1,9 +1,7 @@
package api
import (
"database/sql"
"encoding/base64"
"fmt"
"testing"
log "github.com/hashicorp/go-hclog"
@@ -15,11 +13,9 @@ import (
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/helper/builtinplugins"
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/ory/dockertest"
)
// testVaultServer creates a test vault cluster and returns a configured API
@@ -83,40 +79,3 @@ func testVaultServerCoreConfig(t testing.TB, coreConfig *vault.CoreConfig) (*api
return client, unsealKeys, func() { defer cluster.Cleanup() }
}
// testPostgresDB creates a testing postgres database in a Docker container,
// returning the connection URL and the associated closer function.
func testPostgresDB(t testing.TB) (string, func()) {
pool, err := dockertest.NewPool("")
if err != nil {
t.Fatalf("postgresdb: 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("postgresdb: could not start container: %s", err)
}
cleanup := func() {
docker.CleanupResource(t, pool, resource)
}
addr := fmt.Sprintf("postgres://postgres:secret@localhost:%s/database?sslmode=disable", resource.GetPort("5432/tcp"))
if err := pool.Retry(func() error {
db, err := sql.Open("postgres", addr)
if err != nil {
return err
}
defer db.Close()
return db.Ping()
}); err != nil {
cleanup()
t.Fatalf("postgresdb: could not connect: %s", err)
}
return addr, cleanup
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/hashicorp/vault/api"
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
)
func TestRenewer_Renew(t *testing.T) {
@@ -89,8 +90,8 @@ func TestRenewer_Renew(t *testing.T) {
t.Run("database", func(t *testing.T) {
t.Parallel()
pgURL, pgDone := testPostgresDB(t)
defer pgDone()
cleanup, pgURL := postgreshelper.PrepareTestContainer(t, "")
defer cleanup()
if err := client.Sys().Mount("database", &api.MountInput{
Type: "database",

View File

@@ -1,86 +1,25 @@
# MongoDB Atlas Database Secrets Engine
# HashiCorp Vault Database Secrets Engine - MongoDB Atlas plugin
This plugin provides unique, short-lived credentials for [MongoDB Atlas](https://www.mongodb.com/cloud/atlas).
It is to be used with [Hashicorp Vault](https://www.github.com/hashicorp/vault).
MongoDB Atlas is one of the supported plugins for the HashiCorp Vault Database Secrets Engine and allows for the programmatic generation of unique, ephemeral MongoDB [Database User](https://docs.atlas.mongodb.com/reference/api/database-users/) credentials in MongoDB Atlas Projects.
**Please note**: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault, _please responsibly disclose_ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com).
**The plugin is included in version 1.4 of Vault.**
## Support, Bugs and Feature Requests
Support for the HashiCorp Vault Database Secrets Engine - MongoDB Atlas is provided under MongoDB Atlas support plans. Please submit support questions within the Atlas UI. Vault support is via HashiCorp.
Bugs should be filed under the Issues section of this repo.
Feature requests can be submitted in the Issues section or directly to MongoDB at https://feedback.mongodb.com/forums/924145-atlas - just select the Vault plugin as the category or vote for an already suggested feature.
## Quick Links
- [Database Secrets Engine for MongoDB Atlas](https://www.vaultproject.io/docs/secrets/databases/mongodbatlas.html)
- [Database Secrets Engine for MongoDB Atlas - Docs](https://www.vaultproject.io/docs/secrets/databases/mongodbatlas)
- [Database Secrets Engine for MongoDB Atlas - API Docs](https://www.vaultproject.io/api-docs/secret/databases/mongodbatlas/)
- [MongoDB Atlas Website](https://www.mongodb.com/cloud/atlas)
- [Vault Website](https://www.vaultproject.io)
- [Vault Github](https://www.github.com/hashicorp/vault)
## Getting Started
**Please note**: Hashicorp takes Vault's security and their users' trust very seriously, as does MongoDB.
This is a Vault plugin and is meant to work with Vault. This guide assumes you have already installed Vault
and have a basic understanding of how Vault works.
Otherwise, first read this guide on how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html).
To learn specifically about how plugins work, see documentation on [Vault plugins](https://www.vaultproject.io/docs/internals/plugins.html).
## Installation
This plugin is bundled in Vault version 1.4.0 or later. It may also be built and mounted externally
with earlier versions of Vault. For details on this process please see the documentation for Vault's
[plugin system](https://www.vaultproject.io/docs/internals/plugins.html).
## Setup
1. Enable the database secrets engine if it is not already enabled:
```text
$ vault secrets enable database
Success! Enabled the database secrets engine at: database/
```
The secrets engine will be enabled at the default path which is name of the engine. To
enable the secrets engine at a different path use the `-path` argument.
1. Configure Vault with the proper plugin and connection information:
```text
$ vault write database/config/my-mongodbatlas-database \
plugin_name=mongodbatlas-database-plugin \
allowed_roles="my-role" \
public_key="a-public-key" \
private_key="a-private-key!" \
project_id="a-project-id"
```
2. Configure a role that maps a name in Vault to a MongoDB Atlas command that executes and
creates the Database User credential:
```text
$ vault write database/roles/my-role \
db_name=my-mongodbatlas-database \
creation_statements='{ "database_name": "admin", "roles": [{"databaseName":"admin","roleName":"atlasAdmin"}]}' \
default_ttl="1h" \
max_ttl="24h"
Success! Data written to: database/roles/my-role
```
## Usage
After the secrets engine is configured and a user/machine has a Vault token with
the proper permissions, it can generate credentials.
1. Generate a new credential by reading from the `/creds` endpoint with the name
of the role:
```text
$ vault read database/creds/my-role
Key Value
--- -----
lease_id database/creds/my-role/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6
lease_duration 1h
lease_renewable true
password A1a-QwxApKgnfCp1AJYN
username v-5WFTBKdwOTLOqWLgsjvH-1565815206
```
For more details on configuring and using the plugin, refer to the [Database Secrets Engine for MongoDB Atlas](https://www.vaultproject.io/docs/secrets/databases/mongodbatlas.html)
documentation.
If you believe you have found a security issue in Vault or with this plugin, _please responsibly disclose_ by
contacting HashiCorp at [security@hashicorp.com](mailto:security@hashicorp.com) and contact MongoDB
directly via [security@mongodb.com](mailto:security@mongodb.com) or
[open a ticket](https://jira.mongodb.org/plugins/servlet/samlsso?redirectTo=%2Fbrowse%2FSECURITY) (link is external).

View File

@@ -6,8 +6,8 @@ require (
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a
github.com/go-test/deep v1.0.2 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/mongodb/go-client-mongodb-atlas v0.1.2

View File

@@ -75,12 +75,12 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02 h1:OGEV0U0+lb8SP5aZA1m456Sr3MYxFel2awVr55QRri0=
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820 h1:biZidYDDEWnuOI9mXnJre8lwHKhb5ym85aSXk3oz/dc=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500 h1:tiMX2ewq4ble+e2zENzBvaH2dMoFHe80NbnrF5Ir9Kk=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02 h1:vVrOAVfunVvkTkE9iF3Fe1+PGPLwGIp3nP4qgHGrHFs=
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820 h1:TmDZ1sS6gU0hFeFlFuyJVUwRPEzifZIHCBeS2WF2uSc=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=

View File

@@ -35,9 +35,9 @@ func new() *MongoDBAtlas {
connProducer.Type = mongoDBAtlasTypeName
credsProducer := &credsutil.SQLCredentialsProducer{
DisplayNameLen: 15,
DisplayNameLen: credsutil.NoneLength,
RoleNameLen: 15,
UsernameLen: 100,
UsernameLen: 20,
Separator: "-",
}
@@ -149,12 +149,6 @@ func (m *MongoDBAtlas) SetCredentials(ctx context.Context, statements dbplugin.S
m.Lock()
defer m.Unlock()
statements = dbutil.StatementCompatibilityHelper(statements)
if len(statements.Creation) == 0 {
return "", "", dbutil.ErrEmptyCreationStatement
}
client, err := m.getConnection(ctx)
if err != nil {
return "", "", err

2
vendor/modules.txt vendored
View File

@@ -431,7 +431,7 @@ github.com/hashicorp/vault-plugin-auth-kubernetes
github.com/hashicorp/vault-plugin-auth-oci
# github.com/hashicorp/vault-plugin-database-elasticsearch v0.5.4
github.com/hashicorp/vault-plugin-database-elasticsearch
# github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.1
# github.com/hashicorp/vault-plugin-database-mongodbatlas v0.1.0-beta1.0.20200521152755-9cf156a44f9c
github.com/hashicorp/vault-plugin-database-mongodbatlas
# github.com/hashicorp/vault-plugin-secrets-ad v0.6.4-beta1.0.20200518124111-3dceeb3ce90e
github.com/hashicorp/vault-plugin-secrets-ad/plugin