vault/builtin/logical/database/rotation_test.go
Christopher Swenson a65d9133a1
database: Avoid race condition in connection creation (#26147)
When creating database connections, there is a race
condition when multiple goroutines try to create the
connection at the same time. This happens, for
example, on leadership changes in a cluster.

Normally, the extra database connections are cleaned
up when this is detected. However, some database
implementations, notably Postgres, do not seem to
clean up in a timely manner, and can leak in these
scenarios.

To fix this, we create a global lock when creating
database connections to prevent multiple connections
from being created at the same time.

We also clean up the logic at the end so that
if (somehow) we ended up creating an additional
connection, we use the existing one rather than
the new one. This by itself would solve our
problem long-term, however, would still involve
many transient database connections being created
and immediately killed on leadership changes.

It's not ideal to have a single global lock for
database connection creation. Some potential
alternatives:

* a map of locks from the connection name to the lock.
  The biggest downside is the we probably will want to
  garbage collect this map so that we don't have an
  unbounded number of locks.
* a small pool of locks, where we hash the connection
  names to pick the lock. Using such a pool generally
  is a good way to introduce deadlock, but since we
  will only use it in a specific case, and the purpose
  is to improve performance for concurrent connection
  creation, this is probably acceptable.

Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com>
2024-03-26 16:58:07 +00:00

1809 lines
52 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package database
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"os"
"strings"
"testing"
"time"
"github.com/Sectorbob/mlab-ns2/gae/ns/digest"
"github.com/hashicorp/vault/builtin/logical/database/schedule"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers/mongodb"
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/robfig/cron/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
mongodbatlasapi "go.mongodb.org/atlas/mongodbatlas"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
mockv5 = "mockv5"
dbUser = "vaultstatictest"
dbUserDefaultPassword = "password"
testMinRotationWindowSeconds = 5
testScheduleParseOptions = cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow
)
func TestBackend_StaticRole_Rotation_basic(t *testing.T) {
cluster, sys := getClusterPostgresDB(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())
b.schedule = &TestSchedule{}
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)
}
testCases := map[string]struct {
account map[string]interface{}
path string
expected map[string]interface{}
waitTime time.Duration
}{
"basic with rotation_period": {
account: map[string]interface{}{
"username": dbUser,
"rotation_period": "5400s",
},
path: "plugin-role-test-1",
expected: map[string]interface{}{
"username": dbUser,
"rotation_period": float64(5400),
},
},
"rotation_schedule is set and expires": {
account: map[string]interface{}{
"username": dbUser,
"rotation_schedule": "*/10 * * * * *",
},
path: "plugin-role-test-2",
expected: map[string]interface{}{
"username": dbUser,
"rotation_schedule": "*/10 * * * * *",
},
waitTime: 20 * time.Second,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"rotation_statements": testRoleStaticUpdate,
"username": dbUser,
}
for k, v := range tc.account {
data[k] = v
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + tc.path,
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/" + tc.path,
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/" + tc.path,
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/" + tc.path,
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/" + tc.path,
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)
if tc.waitTime > 0 {
time.Sleep(tc.waitTime)
// Re-Read the creds after schedule expiration
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/" + tc.path,
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)
}
checkPassword := resp.Data["password"].(string)
if newPassword == checkPassword {
t.Fatalf("expected passwords to differ, got (%s)", checkPassword)
}
}
})
}
}
// TestBackend_StaticRole_Rotation_Schedule_ErrorRecover tests that failed
// rotations can successfully recover and that they do not occur outside of a
// rotation window.
func TestBackend_StaticRole_Rotation_Schedule_ErrorRecover(t *testing.T) {
cluster, sys := getClusterPostgresDB(t)
t.Cleanup(cluster.Cleanup)
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
eventSender := logical.NewMockEventSender()
config.EventsSender = eventSender
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())
b.schedule = &TestSchedule{}
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
t.Cleanup(cleanup)
// create the database user
createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate)
verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL)
// Configure a connection
connectionData := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
configureConnection(t, b, config.StorageView, connectionData)
// create the role that will rotate every 10th second
// rotations will not be allowed after 5s
data := map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"rotation_statements": testRoleStaticUpdate,
"rotation_schedule": "*/10 * * * * *",
"rotation_window": "5s",
"username": dbUser,
}
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
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/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)
}
username := resp.Data["username"].(string)
originalPassword := resp.Data["password"].(string)
if username == "" || originalPassword == "" {
t.Fatalf("empty username (%s) or password (%s)", username, originalPassword)
}
// Verify username/password
verifyPgConn(t, dbUser, originalPassword, connURL)
// Set invalid connection URL so we fail to rotate
connectionData["connection_url"] = strings.Replace(connURL, "postgres:secret", "postgres:foo", 1)
configureConnection(t, b, config.StorageView, connectionData)
// determine next rotation schedules based on current test time
rotationSchedule := data["rotation_schedule"].(string)
schedule, err := b.schedule.Parse(rotationSchedule)
if err != nil {
t.Fatalf("could not parse rotation_schedule: %s", err)
}
next := schedule.Next(time.Now()) // the next rotation time we expect
time.Sleep(next.Sub(time.Now()))
// Re-Read the creds after schedule expiration
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/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)
}
checkPassword := resp.Data["password"].(string)
if originalPassword != checkPassword {
// should match because rotations should be failing
t.Fatalf("expected passwords to match, got (%s)", checkPassword)
}
// wait until we are outside the rotation window so that rotations will not occur
next = schedule.Next(time.Now()) // the next rotation time after now
time.Sleep(next.Add(time.Second * 6).Sub(time.Now()))
// reset to valid connection URL so we do not fail to rotate anymore
connectionData["connection_url"] = connURL
configureConnection(t, b, config.StorageView, connectionData)
// we are outside a rotation window, Re-Read the creds
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/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)
}
checkPassword = resp.Data["password"].(string)
if originalPassword != checkPassword {
// should match because rotations should not occur outside the rotation window
t.Fatalf("expected passwords to match, got (%s)", checkPassword)
}
// Verify new username/password
verifyPgConn(t, username, checkPassword, connURL)
// sleep until the next rotation time with a buffer to ensure we had time to rotate
next = schedule.Next(time.Now()) // the next rotation time we expect
time.Sleep(next.Add(time.Second * 5).Sub(time.Now()))
// Re-Read the creds
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/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)
}
checkPassword = resp.Data["password"].(string)
if originalPassword == checkPassword {
// should differ because we slept until the next rotation time
t.Fatalf("expected passwords to differ, got (%s)", checkPassword)
}
// Verify new username/password
verifyPgConn(t, username, checkPassword, connURL)
eventSender.Stop() // avoid race detector
// check that we got a successful rotation event
if len(eventSender.Events) == 0 {
t.Fatal("Expected to have some events but got none")
}
// check that we got a rotate-fail event
found := false
for _, event := range eventSender.Events {
if string(event.Type) == "database/rotate-fail" {
found = true
break
}
}
assert.True(t, found)
found = false
for _, event := range eventSender.Events {
if string(event.Type) == "database/rotate" {
found = true
break
}
}
assert.True(t, found)
}
// 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_Rotation_NonStaticError(t *testing.T) {
cluster, sys := getClusterPostgresDB(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_Rotation_Revoke_user(t *testing.T) {
cluster, sys := getClusterPostgresDB(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")
db, err := sql.Open("pgx", connURL)
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.ExecuteTxQueryDirect(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("pgx", 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_StaticRole_Rotation_QueueWAL_discard_role_not_found(t *testing.T) {
cluster, sys := getClusterPostgresDB(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_StaticRole_Rotation_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
cluster, sys := getClusterPostgresDB(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_Rotation_PostgreSQL(t *testing.T) {
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster")
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_Rotation_MongoDB(t *testing.T) {
cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "5.0.10", "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_Rotation_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(), "admin", 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,
})
}
// TestQueueTickIntervalKeyConfig tests the configuration of queueTickIntervalKey
// does not break on invalid values.
func TestQueueTickIntervalKeyConfig(t *testing.T) {
t.Parallel()
cluster, sys := getClusterPostgresDB(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
config.Config[queueTickIntervalKey] = "1"
// Rotation ticker starts running in Factory call
b, err := Factory(context.Background(), config)
require.Nil(t, err)
b.Cleanup(context.Background())
config.Config[queueTickIntervalKey] = "0"
b, err = Factory(context.Background(), config)
require.Nil(t, err)
b.Cleanup(context.Background())
config.Config[queueTickIntervalKey] = "-1"
b, err = Factory(context.Background(), config)
require.Nil(t, err)
b.Cleanup(context.Background())
}
func testBackend_StaticRole_Rotations(t *testing.T, createUser userCreator, opts map[string]interface{}) {
// We need to set this value for the plugin to run, but it doesn't matter what we set it to.
oldToken := os.Getenv(pluginutil.PluginUnwrapTokenEnv)
os.Setenv(pluginutil.PluginUnwrapTokenEnv, "...")
defer func() {
if oldToken != "" {
os.Setenv(pluginutil.PluginUnwrapTokenEnv, oldToken)
} else {
os.Unsetenv(pluginutil.PluginUnwrapTokenEnv)
}
}()
cluster, sys := getClusterPostgresDB(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_Rotation_LockRegression(t *testing.T) {
cluster, sys := getClusterPostgresDB(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)
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.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)
}
for i := 0; i < 25; i++ {
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_Rotation_Invalid_Role(t *testing.T) {
cluster, sys := getClusterPostgresDB(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())
}
}
func TestRollsPasswordForwardsUsingWAL(t *testing.T) {
ctx := context.Background()
b, storage, mockDB := getBackend(t)
defer b.Cleanup(ctx)
configureDBMount(t, storage)
createRole(t, b, storage, mockDB, "hashicorp")
role, err := b.StaticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
oldPassword := role.StaticAccount.Password
generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp")
walIDs := requireWALs(t, storage, 1)
wal, err := b.findStaticWAL(ctx, storage, walIDs[0])
if err != nil {
t.Fatal(err)
}
role, err = b.StaticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
// Role's password should still be the WAL's old password
if role.StaticAccount.Password != oldPassword {
t.Fatal(role.StaticAccount.Password, oldPassword)
}
rotateRole(t, b, storage, mockDB, "hashicorp")
role, err = b.StaticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
if role.StaticAccount.Password != wal.NewPassword {
t.Fatal("role password", role.StaticAccount.Password, "WAL new password", wal.NewPassword)
}
// WAL should be cleared by the successful rotate
requireWALs(t, storage, 0)
}
func TestStoredWALsCorrectlyProcessed(t *testing.T) {
const walNewPassword = "new-password-from-wal"
rotationPeriodData := map[string]interface{}{
"username": "hashicorp",
"db_name": mockv5,
"rotation_period": "86400s",
}
for _, tc := range []struct {
name string
shouldRotate bool
wal *setCredentialsWAL
data map[string]interface{}
}{
{
"WAL is kept and used for roll forward",
true,
&setCredentialsWAL{
RoleName: "hashicorp",
Username: "hashicorp",
NewPassword: walNewPassword,
LastVaultRotation: time.Now().Add(time.Hour),
},
rotationPeriodData,
},
{
"zero-time WAL is discarded on load",
false,
&setCredentialsWAL{
RoleName: "hashicorp",
Username: "hashicorp",
NewPassword: walNewPassword,
LastVaultRotation: time.Time{},
},
rotationPeriodData,
},
{
"rotation_period empty-password WAL is kept but a new password is generated",
true,
&setCredentialsWAL{
RoleName: "hashicorp",
Username: "hashicorp",
NewPassword: "",
LastVaultRotation: time.Now().Add(time.Hour),
},
rotationPeriodData,
},
{
"rotation_schedule empty-password WAL is kept but a new password is generated",
true,
&setCredentialsWAL{
RoleName: "hashicorp",
Username: "hashicorp",
NewPassword: "",
LastVaultRotation: time.Now().Add(time.Hour),
},
map[string]interface{}{
"username": "hashicorp",
"db_name": mockv5,
"rotation_schedule": "*/10 * * * * *",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b := Backend(config)
defer b.Cleanup(ctx)
mockDB := setupMockDB(b)
if err := b.Setup(ctx, config); err != nil {
t.Fatal(err)
}
b.credRotationQueue = queue.New()
b.schedule = &TestSchedule{}
configureDBMount(t, config.StorageView)
createRoleWithData(t, b, config.StorageView, mockDB, tc.wal.RoleName, tc.data)
role, err := b.StaticRole(ctx, config.StorageView, "hashicorp")
if err != nil {
t.Fatal(err)
}
initialPassword := role.StaticAccount.Password
// Set up a WAL for our test case
framework.PutWAL(ctx, config.StorageView, staticWALKey, tc.wal)
requireWALs(t, config.StorageView, 1)
// Reset the rotation queue to simulate startup memory state
b.credRotationQueue = queue.New()
// Now finish the startup process by populating the queue, which should discard the WAL
b.initQueue(ctx, config)
if tc.shouldRotate {
requireWALs(t, storage, 1)
} else {
requireWALs(t, storage, 0)
}
// Run one tick
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
Return(v5.UpdateUserResponse{}, nil).
Once()
b.rotateCredentials(ctx, storage)
requireWALs(t, storage, 0)
role, err = b.StaticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
item, err := b.popFromRotationQueueByKey("hashicorp")
if err != nil {
t.Fatal(err)
}
nextRotationTime := role.StaticAccount.NextRotationTime()
if tc.shouldRotate {
if tc.wal.NewPassword != "" {
// Should use WAL's new_password field
if role.StaticAccount.Password != walNewPassword {
t.Fatal()
}
} else {
// Should rotate but ignore WAL's new_password field
if role.StaticAccount.Password == initialPassword {
t.Fatal()
}
if role.StaticAccount.Password == walNewPassword {
t.Fatal()
}
}
// Ensure the role was not promoted for early rotation
assertPriorityUnchanged(t, item.Priority, nextRotationTime)
} else {
// Ensure the role was not promoted for early rotation
assertPriorityUnchanged(t, item.Priority, nextRotationTime)
if role.StaticAccount.Password != initialPassword {
t.Fatal("password should not have been rotated yet")
}
}
})
}
}
func TestDeletesOlderWALsOnLoad(t *testing.T) {
ctx := context.Background()
b, storage, mockDB := getBackend(t)
defer b.Cleanup(ctx)
configureDBMount(t, storage)
createRole(t, b, storage, mockDB, "hashicorp")
// Create 4 WALs, with a clear winner for most recent.
wal := &setCredentialsWAL{
RoleName: "hashicorp",
Username: "hashicorp",
NewPassword: "some-new-password",
LastVaultRotation: time.Now(),
}
for i := 0; i < 3; i++ {
_, err := framework.PutWAL(ctx, storage, staticWALKey, wal)
if err != nil {
t.Fatal(err)
}
}
time.Sleep(2 * time.Second)
// We expect this WAL to have the latest createdAt timestamp
walID, err := framework.PutWAL(ctx, storage, staticWALKey, wal)
if err != nil {
t.Fatal(err)
}
requireWALs(t, storage, 4)
walMap, err := b.loadStaticWALs(ctx, storage)
if err != nil {
t.Fatal(err)
}
if len(walMap) != 1 || walMap["hashicorp"] == nil || walMap["hashicorp"].walID != walID {
t.Fatal()
}
requireWALs(t, storage, 1)
}
func generateWALFromFailedRotation(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
t.Helper()
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
Return(v5.UpdateUserResponse{}, errors.New("forced error")).
Once()
_, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/" + roleName,
Storage: storage,
})
if err == nil {
t.Fatal("expected error")
}
}
func rotateRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
t.Helper()
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
Return(v5.UpdateUserResponse{}, nil).
Once()
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/" + roleName,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(resp, err)
}
}
// returns a slice of the WAL IDs in storage
func requireWALs(t *testing.T, storage logical.Storage, expectedCount int) []string {
t.Helper()
wals, err := storage.List(context.Background(), "wal/")
if err != nil {
t.Fatal(err)
}
if len(wals) != expectedCount {
t.Fatal("expected WALs", expectedCount, "got", len(wals))
}
return wals
}
func getBackend(t *testing.T) (*databaseBackend, logical.Storage, *mockNewDatabase) {
t.Helper()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
// Create and init the backend ourselves instead of using a Factory because
// the factory function kicks off threads that cause racy tests.
b := Backend(config)
if err := b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}
b.schedule = &TestSchedule{}
b.credRotationQueue = queue.New()
b.populateQueue(context.Background(), config.StorageView)
mockDB := setupMockDB(b)
return b, config.StorageView, mockDB
}
func setupMockDB(b *databaseBackend) *mockNewDatabase {
mockDB := &mockNewDatabase{}
mockDB.On("Initialize", mock.Anything, mock.Anything).Return(v5.InitializeResponse{}, nil)
mockDB.On("Close").Return(nil)
mockDB.On("Type").Return("mock", nil)
dbw := databaseVersionWrapper{
v5: mockDB,
}
dbi := &dbPluginInstance{
database: dbw,
id: "foo-id",
name: mockv5,
}
b.connections.Put(mockv5, dbi)
return mockDB
}
// configureDBMount puts config directly into storage to avoid the DB engine's
// plugin init code paths, allowing us to use a manually populated mock DB object.
func configureDBMount(t *testing.T, storage logical.Storage) {
t.Helper()
entry, err := logical.StorageEntryJSON(fmt.Sprintf("config/"+mockv5), &DatabaseConfig{
AllowedRoles: []string{"*"},
})
if err != nil {
t.Fatal(err)
}
err = storage.Put(context.Background(), entry)
if err != nil {
t.Fatal(err)
}
}
// 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 configureConnection(t *testing.T, b *databaseBackend, s logical.Storage, data map[string]interface{}) {
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/" + data["name"].(string),
Storage: s,
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)
}
}
func newBoolPtr(b bool) *bool {
v := b
return &v
}
// assertPriorityUnchanged is a helper to verify that the priority is the
// expected value for a given rotation time
func assertPriorityUnchanged(t *testing.T, priority int64, nextRotationTime time.Time) {
t.Helper()
if priority != nextRotationTime.Unix() {
t.Fatalf("expected next rotation at %s, but got %s", nextRotationTime, time.Unix(priority, 0).String())
}
}
var _ schedule.Scheduler = &TestSchedule{}
type TestSchedule struct{}
func (d *TestSchedule) Parse(rotationSchedule string) (*cron.SpecSchedule, error) {
parser := cron.NewParser(testScheduleParseOptions)
schedule, err := parser.Parse(rotationSchedule)
if err != nil {
return nil, err
}
sched, ok := schedule.(*cron.SpecSchedule)
if !ok {
return nil, fmt.Errorf("invalid rotation schedule")
}
return sched, nil
}
func (d *TestSchedule) ValidateRotationWindow(s int) error {
if s < testMinRotationWindowSeconds {
return fmt.Errorf("rotation_window must be %d seconds or more", testMinRotationWindowSeconds)
}
return nil
}