423 lines
12 KiB
Go
423 lines
12 KiB
Go
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.Replace(connURL, "postgres:secret", "{{username}}:{{password}}", -1)
|
|
|
|
// 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.Replace(connURL, "postgres:secret", "{{username}}:{{password}}", -1)
|
|
|
|
// 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.Replace(connURL, "postgres:secret", "{{username}}:{{password}}", -1)
|
|
|
|
// 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)
|
|
}
|
|
}
|