1133 lines
32 KiB
Go
1133 lines
32 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
dbUser = "vaultstatictest"
|
|
dbUserDefaultPassword = "password"
|
|
|
|
testMongoDBRole = `{ "db": "admin", "roles": [ { "role": "readWrite" } ] }`
|
|
)
|
|
|
|
func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// create the database user
|
|
createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate)
|
|
|
|
verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL)
|
|
|
|
// 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)
|
|
}
|
|
|
|
data = map[string]interface{}{
|
|
"name": "plugin-role-test",
|
|
"db_name": "plugin-test",
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"username": dbUser,
|
|
"rotation_period": "5400s",
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "static-roles/plugin-role-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)
|
|
}
|
|
|
|
// Read the creds
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/plugin-role-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)
|
|
}
|
|
|
|
username := resp.Data["username"].(string)
|
|
password := resp.Data["password"].(string)
|
|
if username == "" || password == "" {
|
|
t.Fatalf("empty username (%s) or password (%s)", username, password)
|
|
}
|
|
|
|
// Verify username/password
|
|
verifyPgConn(t, dbUser, password, connURL)
|
|
|
|
// Re-read the creds, verifying they aren't changing on read
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/plugin-role-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)
|
|
}
|
|
|
|
if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) {
|
|
t.Fatal("expected re-read username/password to match, but didn't")
|
|
}
|
|
|
|
// Trigger rotation
|
|
data = map[string]interface{}{"name": "plugin-role-test"}
|
|
req = &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "rotate-role/plugin-role-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)
|
|
}
|
|
|
|
if resp != nil {
|
|
t.Fatalf("Expected empty response from rotate-role: (%#v)", resp)
|
|
}
|
|
|
|
// Re-Read the creds
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/plugin-role-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)
|
|
}
|
|
|
|
newPassword := resp.Data["password"].(string)
|
|
if password == newPassword {
|
|
t.Fatalf("expected passwords to differ, got (%s)", newPassword)
|
|
}
|
|
|
|
// Verify new username/password
|
|
verifyPgConn(t, username, newPassword, connURL)
|
|
}
|
|
|
|
// Sanity check to make sure we don't allow an attempt of rotating credentials
|
|
// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to
|
|
// verify we return an error
|
|
func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// create the database user
|
|
createTestPGUser(t, connURL, dbUser, 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)
|
|
}
|
|
|
|
data = map[string]interface{}{
|
|
"name": "plugin-role-test",
|
|
"db_name": "plugin-test",
|
|
"creation_statements": testRoleStaticCreate,
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"revocation_statements": defaultRevocationSQL,
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "roles/plugin-role-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)
|
|
}
|
|
|
|
// Read the creds
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "creds/plugin-role-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)
|
|
}
|
|
|
|
username := resp.Data["username"].(string)
|
|
password := resp.Data["password"].(string)
|
|
if username == "" || password == "" {
|
|
t.Fatalf("empty username (%s) or password (%s)", username, password)
|
|
}
|
|
|
|
// Verify username/password
|
|
verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL)
|
|
// Trigger rotation
|
|
data = map[string]interface{}{"name": "plugin-role-test"}
|
|
req = &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "rotate-role/plugin-role-test",
|
|
Storage: config.StorageView,
|
|
Data: data,
|
|
}
|
|
// expect resp to be an error
|
|
resp, _ = b.HandleRequest(namespace.RootContext(nil), req)
|
|
if !resp.IsError() {
|
|
t.Fatalf("expected error rotating non-static role")
|
|
}
|
|
|
|
if resp.Error().Error() != "no static role found for role name" {
|
|
t.Fatalf("wrong error message: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestBackend_StaticRole_Revoke_user(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// create the database user
|
|
createTestPGUser(t, connURL, dbUser, 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)
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
revoke *bool
|
|
expectVerifyErr bool
|
|
}{
|
|
// Default case: user does not specify, Vault leaves the database user
|
|
// untouched, and the final connection check passes because the user still
|
|
// exists
|
|
"unset": {},
|
|
// Revoke on delete. The final connection check should fail because the user
|
|
// no longer exists
|
|
"revoke": {
|
|
revoke: newBoolPtr(true),
|
|
expectVerifyErr: true,
|
|
},
|
|
// Revoke false, final connection check should still pass
|
|
"persist": {
|
|
revoke: newBoolPtr(false),
|
|
},
|
|
}
|
|
for k, tc := range testCases {
|
|
t.Run(k, func(t *testing.T) {
|
|
data = map[string]interface{}{
|
|
"name": "plugin-role-test",
|
|
"db_name": "plugin-test",
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"username": dbUser,
|
|
"rotation_period": "5400s",
|
|
}
|
|
if tc.revoke != nil {
|
|
data["revoke_user_on_delete"] = *tc.revoke
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "static-roles/plugin-role-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)
|
|
}
|
|
|
|
// Read the creds
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/plugin-role-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)
|
|
}
|
|
|
|
username := resp.Data["username"].(string)
|
|
password := resp.Data["password"].(string)
|
|
if username == "" || password == "" {
|
|
t.Fatalf("empty username (%s) or password (%s)", username, password)
|
|
}
|
|
|
|
// Verify username/password
|
|
verifyPgConn(t, username, password, connURL)
|
|
|
|
// delete the role, expect the default where the user is not destroyed
|
|
// Read the creds
|
|
req = &logical.Request{
|
|
Operation: logical.DeleteOperation,
|
|
Path: "static-roles/plugin-role-test",
|
|
Storage: config.StorageView,
|
|
}
|
|
|
|
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 new username/password still work
|
|
verifyPgConn(t, username, password, connURL)
|
|
})
|
|
}
|
|
}
|
|
|
|
func createTestPGUser(t *testing.T, connURL string, username, password, query string) {
|
|
t.Helper()
|
|
log.Printf("[TRACE] Creating test user")
|
|
conn, err := pq.ParseURL(connURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db, err := sql.Open("postgres", conn)
|
|
defer db.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Start a transaction
|
|
ctx := context.Background()
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
m := map[string]string{
|
|
"name": username,
|
|
"password": password,
|
|
}
|
|
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Commit the transaction
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func verifyPgConn(t *testing.T, username, password, connURL string) {
|
|
t.Helper()
|
|
cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1)
|
|
db, err := sql.Open("postgres", cURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Ping(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// WAL testing
|
|
//
|
|
// First scenario, WAL contains a role name that does not exist.
|
|
func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
_, err := framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
|
|
RoleName: "doesnotexist",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error with PutWAL: %s", err)
|
|
}
|
|
|
|
assertWALCount(t, config.StorageView, 1, staticWALKey)
|
|
|
|
b, err := Factory(ctx, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer b.Cleanup(ctx)
|
|
|
|
time.Sleep(5 * time.Second)
|
|
bd := b.(*databaseBackend)
|
|
if bd.credRotationQueue == nil {
|
|
t.Fatal("database backend had no credential rotation queue")
|
|
}
|
|
|
|
// Verify empty queue
|
|
if bd.credRotationQueue.Len() != 0 {
|
|
t.Fatalf("expected zero queue items, got: %d", bd.credRotationQueue.Len())
|
|
}
|
|
|
|
assertWALCount(t, config.StorageView, 0, staticWALKey)
|
|
}
|
|
|
|
// Second scenario, WAL contains a role name that does exist, but the role's
|
|
// LastVaultRotation is greater than the WAL has
|
|
func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
|
|
t.Skip("temporarily disabled due to intermittent failures")
|
|
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
roleName := "test-discard-by-date"
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// create the database user
|
|
createTestPGUser(t, connURL, dbUser, 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)
|
|
}
|
|
|
|
// Save Now() to make sure rotation time is after this, as well as the WAL
|
|
// time
|
|
roleTime := time.Now()
|
|
|
|
// Create role
|
|
data = map[string]interface{}{
|
|
"name": roleName,
|
|
"db_name": "plugin-test",
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"username": dbUser,
|
|
// Low value here, to make sure the backend rotates this password at least
|
|
// once before we compare it to the WAL
|
|
"rotation_period": "10s",
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Allow the first rotation to occur, setting LastVaultRotation
|
|
time.Sleep(time.Second * 12)
|
|
|
|
// Cleanup the backend, then create a WAL for the role with a
|
|
// LastVaultRotation of 1 hour ago, so that when we recreate the backend the
|
|
// WAL will be read but discarded
|
|
b.Cleanup(ctx)
|
|
b = nil
|
|
time.Sleep(time.Second * 3)
|
|
|
|
// Make a fake WAL entry with an older time
|
|
oldRotationTime := roleTime.Add(time.Hour * -1)
|
|
walPassword := "somejunkpassword"
|
|
_, err = framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
|
|
RoleName: roleName,
|
|
NewPassword: walPassword,
|
|
LastVaultRotation: oldRotationTime,
|
|
Username: dbUser,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error with PutWAL: %s", err)
|
|
}
|
|
|
|
assertWALCount(t, config.StorageView, 1, staticWALKey)
|
|
|
|
// Reload backend
|
|
lb, err = Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok = lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(ctx)
|
|
|
|
// Allow enough time for populateQueue to work after boot
|
|
time.Sleep(time.Second * 12)
|
|
|
|
// PopulateQueue should have processed the entry
|
|
assertWALCount(t, config.StorageView, 0, staticWALKey)
|
|
|
|
// Read the role
|
|
data = map[string]interface{}{}
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
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)
|
|
}
|
|
|
|
lastVaultRotation := resp.Data["last_vault_rotation"].(time.Time)
|
|
if !lastVaultRotation.After(oldRotationTime) {
|
|
t.Fatal("last vault rotation time not greater than WAL time")
|
|
}
|
|
|
|
if !lastVaultRotation.After(roleTime) {
|
|
t.Fatal("last vault rotation time not greater than role creation time")
|
|
}
|
|
|
|
// Grab password to verify it didn't change
|
|
req = &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/" + roleName,
|
|
Storage: config.StorageView,
|
|
}
|
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
|
}
|
|
|
|
password := resp.Data["password"].(string)
|
|
if password == walPassword {
|
|
t.Fatalf("expected password to not be changed by WAL, but was")
|
|
}
|
|
}
|
|
|
|
// Helper to assert the number of WAL entries is what we expect
|
|
func assertWALCount(t *testing.T, s logical.Storage, expected int, key string) {
|
|
t.Helper()
|
|
|
|
var count int
|
|
ctx := context.Background()
|
|
keys, err := framework.ListWAL(ctx, s)
|
|
if err != nil {
|
|
t.Fatal("error listing WALs")
|
|
}
|
|
|
|
// Loop through WAL keys and process any rotation ones
|
|
for _, k := range keys {
|
|
walEntry, _ := framework.GetWAL(ctx, s, k)
|
|
if walEntry == nil {
|
|
continue
|
|
}
|
|
|
|
if walEntry.Kind != key {
|
|
continue
|
|
}
|
|
count++
|
|
}
|
|
if expected != count {
|
|
t.Fatalf("WAL count mismatch, expected (%d), got (%d)", expected, count)
|
|
}
|
|
}
|
|
|
|
//
|
|
// End WAL testing
|
|
//
|
|
|
|
type userCreator func(t *testing.T, username, password string)
|
|
|
|
func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest")
|
|
defer cleanup()
|
|
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()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
// Change background task interval to 1s to give more margin
|
|
// for it to successfully run during the sleeps below.
|
|
config.Config[queueTickIntervalKey] = "1"
|
|
|
|
// Rotation ticker starts running in Factory call
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
// allow initQueue to finish
|
|
bd := b.(*databaseBackend)
|
|
if bd.credRotationQueue == nil {
|
|
t.Fatal("database backend had no credential rotation queue")
|
|
}
|
|
|
|
// Configure a connection
|
|
data := map[string]interface{}{
|
|
"verify_connection": false,
|
|
"allowed_roles": []string{"*"},
|
|
}
|
|
for k, v := range opts {
|
|
data[k] = v
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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-test",
|
|
"username": "statictest" + 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 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 sr10 and sr20 to rotate
|
|
time.Sleep(10 * time.Second)
|
|
pws = capturePasswords(t, b, config, testCases, pws)
|
|
|
|
// verify all pws are as they should
|
|
pass := true
|
|
for k, v := range pws {
|
|
if len(v) < 3 {
|
|
t.Fatalf("expected to find 3 passwords for (%s), only found (%d)", k, len(v))
|
|
}
|
|
switch {
|
|
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-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-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 {
|
|
t.Fatalf("password rotations did not match expected: %#v", pws)
|
|
}
|
|
}
|
|
|
|
func testCreateDBUser(t testing.TB, connURL, db, username, password string) {
|
|
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
|
|
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
createUserCmd := &createUserCommand{
|
|
Username: username,
|
|
Password: password,
|
|
Roles: []interface{}{},
|
|
}
|
|
result := client.Database(db).RunCommand(ctx, createUserCmd, nil)
|
|
if result.Err() != nil {
|
|
t.Fatal(result.Err())
|
|
}
|
|
}
|
|
|
|
type createUserCommand struct {
|
|
Username string `bson:"createUser"`
|
|
Password string `bson:"pwd"`
|
|
Roles []interface{} `bson:"roles"`
|
|
}
|
|
|
|
// Demonstrates a bug fix for the credential rotation not releasing locks
|
|
func TestBackend_StaticRole_LockRegression(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// 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)
|
|
}
|
|
|
|
createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate)
|
|
for i := 0; i < 25; i++ {
|
|
data := map[string]interface{}{
|
|
"name": "plugin-role-test",
|
|
"db_name": "plugin-test",
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"username": dbUser,
|
|
"rotation_period": "7s",
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "static-roles/plugin-role-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)
|
|
}
|
|
|
|
// sleeping is needed to trigger the deadlock, otherwise things are
|
|
// processed too quickly to trigger the rotation lock on so few roles
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestBackend_StaticRole_Rotate_Invalid_Role(t *testing.T) {
|
|
cluster, sys := getCluster(t)
|
|
defer cluster.Cleanup()
|
|
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
config.System = sys
|
|
|
|
lb, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, ok := lb.(*databaseBackend)
|
|
if !ok {
|
|
t.Fatal("could not convert to db backend")
|
|
}
|
|
defer b.Cleanup(context.Background())
|
|
|
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
|
defer cleanup()
|
|
|
|
// create the database user
|
|
createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate)
|
|
|
|
verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL)
|
|
|
|
// 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)
|
|
}
|
|
|
|
data = map[string]interface{}{
|
|
"name": "plugin-role-test",
|
|
"db_name": "plugin-test",
|
|
"rotation_statements": testRoleStaticUpdate,
|
|
"username": dbUser,
|
|
"rotation_period": "5400s",
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "static-roles/plugin-role-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)
|
|
}
|
|
|
|
// Pop manually key to emulate a queue without existing key
|
|
b.credRotationQueue.PopByKey("plugin-role-test")
|
|
|
|
// Make sure queue is empty
|
|
if b.credRotationQueue.Len() != 0 {
|
|
t.Fatalf("expected queue length to be 0 but is %d", b.credRotationQueue.Len())
|
|
}
|
|
|
|
// Trigger rotation
|
|
data = map[string]interface{}{"name": "plugin-role-test"}
|
|
req = &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "rotate-role/plugin-role-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)
|
|
}
|
|
|
|
// Check if key is in queue
|
|
if b.credRotationQueue.Len() != 1 {
|
|
t.Fatalf("expected queue length to be 1 but is %d", b.credRotationQueue.Len())
|
|
}
|
|
}
|
|
|
|
// capturePasswords captures the current passwords at the time of calling, and
|
|
// returns a map of username / passwords building off of the input map
|
|
func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string {
|
|
new := make(map[string][]string, 0)
|
|
for _, tc := range testCases {
|
|
// Read the role
|
|
roleName := "plugin-static-role-" + tc
|
|
req := &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "static-creds/" + roleName,
|
|
Storage: config.StorageView,
|
|
}
|
|
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
|
}
|
|
|
|
username := resp.Data["username"].(string)
|
|
password := resp.Data["password"].(string)
|
|
if username == "" || password == "" {
|
|
t.Fatalf("expected both username/password for (%s), got (%s), (%s)", roleName, username, password)
|
|
}
|
|
new[roleName] = append(new[roleName], password)
|
|
}
|
|
|
|
for k, v := range new {
|
|
pws[k] = append(pws[k], v...)
|
|
}
|
|
|
|
return pws
|
|
}
|
|
|
|
func newBoolPtr(b bool) *bool {
|
|
v := b
|
|
return &v
|
|
}
|