| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package database |
| |
| import ( |
| "context" |
| "strings" |
| "testing" |
| "time" |
| |
| "github.com/hashicorp/vault/helper/namespace" |
| 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/logical" |
| ) |
| |
| const ( |
| databaseUser = "postgres" |
| defaultPassword = "secret" |
| ) |
| |
| // Tests that the WAL rollback function rolls back the database password. |
| // The database password should be rolled back when: |
| // - A WAL entry exists |
| // - Password has been altered on the database |
| // - Password has not been updated in storage |
| func TestBackend_RotateRootCredentials_WAL_rollback(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) |
| } |
| dbBackend, ok := lb.(*databaseBackend) |
| if !ok { |
| t.Fatal("could not convert to db backend") |
| } |
| defer lb.Cleanup(context.Background()) |
| |
| cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") |
| defer cleanup() |
| |
| connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") |
| |
| // Configure a connection to the database |
| data := map[string]interface{}{ |
| "connection_url": connURL, |
| "plugin_name": "postgresql-database-plugin", |
| "allowed_roles": []string{"plugin-role-test"}, |
| "username": databaseUser, |
| "password": defaultPassword, |
| } |
| resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "config/plugin-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Create a role |
| data = map[string]interface{}{ |
| "db_name": "plugin-test", |
| "creation_statements": testRole, |
| "max_ttl": "10m", |
| } |
| resp, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/plugin-role-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Read credentials to verify this initially works |
| credReq := &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "creds/plugin-role-test", |
| Storage: config.StorageView, |
| Data: make(map[string]interface{}), |
| } |
| credResp, err := lb.HandleRequest(context.Background(), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| |
| // Get a connection to the database plugin |
| dbi, err := dbBackend.GetConnection(context.Background(), |
| config.StorageView, "plugin-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Alter the database password so it no longer matches what is in storage |
| ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) |
| defer cancel() |
| updateReq := v5.UpdateUserRequest{ |
| Username: databaseUser, |
| Password: &v5.ChangePassword{ |
| NewPassword: "newSecret", |
| }, |
| } |
| _, err = dbi.database.UpdateUser(ctx, updateReq, false) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Clear the plugin connection to verify we're no longer able to connect |
| err = dbBackend.ClearConnection("plugin-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Reading credentials should no longer work |
| credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) |
| if err == nil { |
| t.Fatalf("expected authentication to fail when reading credentials") |
| } |
| |
| // Put a WAL entry that will be used for rolling back the database password |
| walEntry := &rotateRootCredentialsWAL{ |
| ConnectionName: "plugin-test", |
| UserName: databaseUser, |
| OldPassword: defaultPassword, |
| NewPassword: "newSecret", |
| } |
| _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 1, rotateRootWALKey) |
| |
| // Trigger an immediate RollbackOperation so that the WAL rollback |
| // function can use the WAL entry to roll back the database password |
| _, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.RollbackOperation, |
| Path: "", |
| Storage: config.StorageView, |
| Data: map[string]interface{}{ |
| "immediate": true, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 0, rotateRootWALKey) |
| |
| // Reading credentials should work again after the database |
| // password has been rolled back. |
| credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| } |
| |
| // Tests that the WAL rollback function does not roll back the database password. |
| // The database password should not be rolled back when: |
| // - A WAL entry exists |
| // - Password has not been altered on the database |
| // - Password has not been updated in storage |
| func TestBackend_RotateRootCredentials_WAL_no_rollback_1(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) |
| } |
| defer lb.Cleanup(context.Background()) |
| |
| cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") |
| defer cleanup() |
| |
| connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") |
| |
| // Configure a connection to the database |
| data := map[string]interface{}{ |
| "connection_url": connURL, |
| "plugin_name": "postgresql-database-plugin", |
| "allowed_roles": []string{"plugin-role-test"}, |
| "username": databaseUser, |
| "password": defaultPassword, |
| } |
| resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "config/plugin-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Create a role |
| data = map[string]interface{}{ |
| "db_name": "plugin-test", |
| "creation_statements": testRole, |
| "max_ttl": "10m", |
| } |
| resp, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/plugin-role-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Read credentials to verify this initially works |
| credReq := &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "creds/plugin-role-test", |
| Storage: config.StorageView, |
| Data: make(map[string]interface{}), |
| } |
| credResp, err := lb.HandleRequest(context.Background(), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| |
| // Put a WAL entry |
| walEntry := &rotateRootCredentialsWAL{ |
| ConnectionName: "plugin-test", |
| UserName: databaseUser, |
| OldPassword: defaultPassword, |
| NewPassword: "newSecret", |
| } |
| _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 1, rotateRootWALKey) |
| |
| // Trigger an immediate RollbackOperation |
| _, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.RollbackOperation, |
| Path: "", |
| Storage: config.StorageView, |
| Data: map[string]interface{}{ |
| "immediate": true, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 0, rotateRootWALKey) |
| |
| // Reading credentials should work |
| credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| } |
| |
| // Tests that the WAL rollback function does not roll back the database password. |
| // The database password should not be rolled back when: |
| // - A WAL entry exists |
| // - Password has been altered on the database |
| // - Password has been updated in storage |
| func TestBackend_RotateRootCredentials_WAL_no_rollback_2(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) |
| } |
| dbBackend, ok := lb.(*databaseBackend) |
| if !ok { |
| t.Fatal("could not convert to db backend") |
| } |
| defer lb.Cleanup(context.Background()) |
| |
| cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") |
| defer cleanup() |
| |
| connURL = strings.ReplaceAll(connURL, "postgres:secret", "{{username}}:{{password}}") |
| |
| // Configure a connection to the database |
| data := map[string]interface{}{ |
| "connection_url": connURL, |
| "plugin_name": "postgresql-database-plugin", |
| "allowed_roles": []string{"plugin-role-test"}, |
| "username": databaseUser, |
| "password": defaultPassword, |
| } |
| resp, err := lb.HandleRequest(namespace.RootContext(nil), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "config/plugin-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Create a role |
| data = map[string]interface{}{ |
| "db_name": "plugin-test", |
| "creation_statements": testRole, |
| "max_ttl": "10m", |
| } |
| resp, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/plugin-role-test", |
| Storage: config.StorageView, |
| Data: data, |
| }) |
| if err != nil || (resp != nil && resp.IsError()) { |
| t.Fatalf("err:%s resp:%#v\n", err, resp) |
| } |
| |
| // Read credentials to verify this initially works |
| credReq := &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "creds/plugin-role-test", |
| Storage: config.StorageView, |
| Data: make(map[string]interface{}), |
| } |
| credResp, err := lb.HandleRequest(context.Background(), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| |
| // Get a connection to the database plugin |
| dbi, err := dbBackend.GetConnection(context.Background(), config.StorageView, "plugin-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Alter the database password |
| ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) |
| defer cancel() |
| updateReq := v5.UpdateUserRequest{ |
| Username: databaseUser, |
| Password: &v5.ChangePassword{ |
| NewPassword: "newSecret", |
| }, |
| } |
| _, err = dbi.database.UpdateUser(ctx, updateReq, false) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Update storage with the new password |
| dbConfig, err := dbBackend.DatabaseConfig(context.Background(), config.StorageView, |
| "plugin-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| dbConfig.ConnectionDetails["password"] = "newSecret" |
| entry, err := logical.StorageEntryJSON("config/plugin-test", dbConfig) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = config.StorageView.Put(context.Background(), entry) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Clear the plugin connection to verify we can connect to the database |
| err = dbBackend.ClearConnection("plugin-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Reading credentials should work |
| credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| |
| // Put a WAL entry |
| walEntry := &rotateRootCredentialsWAL{ |
| ConnectionName: "plugin-test", |
| UserName: databaseUser, |
| OldPassword: defaultPassword, |
| NewPassword: "newSecret", |
| } |
| _, err = framework.PutWAL(context.Background(), config.StorageView, rotateRootWALKey, walEntry) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 1, rotateRootWALKey) |
| |
| // Trigger an immediate RollbackOperation |
| _, err = lb.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.RollbackOperation, |
| Path: "", |
| Storage: config.StorageView, |
| Data: map[string]interface{}{ |
| "immediate": true, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| assertWALCount(t, config.StorageView, 0, rotateRootWALKey) |
| |
| // Reading credentials should work |
| credResp, err = lb.HandleRequest(namespace.RootContext(nil), credReq) |
| if err != nil || (credResp != nil && credResp.IsError()) { |
| t.Fatalf("err:%s resp:%v\n", err, credResp) |
| } |
| } |