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/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/consts" "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/lib/pq" "github.com/stretchr/testify/mock" mongodbatlasapi "go.mongodb.org/atlas/mongodbatlas" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) const ( dbUser = "vaultstatictest" dbUserDefaultPassword = "password" ) 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.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("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, "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_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(), "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, }) } 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 := 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) 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_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()) } } 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" for _, tc := range []struct { name string shouldRotate bool wal *setCredentialsWAL }{ { "WAL is kept and used for roll forward", true, &setCredentialsWAL{ RoleName: "hashicorp", Username: "hashicorp", NewPassword: walNewPassword, LastVaultRotation: time.Now().Add(time.Hour), }, }, { "zero-time WAL is discarded on load", false, &setCredentialsWAL{ RoleName: "hashicorp", Username: "hashicorp", NewPassword: walNewPassword, LastVaultRotation: time.Time{}, }, }, { "empty-password WAL is kept but a new password is generated", true, &setCredentialsWAL{ RoleName: "hashicorp", Username: "hashicorp", NewPassword: "", LastVaultRotation: time.Now().Add(time.Hour), }, }, } { 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() configureDBMount(t, config.StorageView) createRole(t, b, config.StorageView, mockDB, "hashicorp") 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, consts.ReplicationUnknown) 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) } 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() } } } else { // Ensure the role was not promoted for early rotation if item.Priority < time.Now().Add(time.Hour).Unix() { t.Fatal("priority should be for about a week away, but was", item.Priority) } 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.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) dbw := databaseVersionWrapper{ v5: mockDB, } dbi := &dbPluginInstance{ database: dbw, id: "foo-id", name: "mockV5", } b.connections["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 newBoolPtr(b bool) *bool { v := b return &v }