open-vault/builtin/logical/database/versioning_large_test.go
John-Michael Faircloth 14e231563f
db plugin multiplexing: add test coverage (#14330)
* db plugin multiplexing: add test coverage

* refactor: pass factory func directly
2022-03-03 08:40:46 -06:00

320 lines
9 KiB
Go

package database
// This file contains all "large"/expensive tests. These are running requests against a running backend
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
func TestPlugin_lifecycle(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v4-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV4", []string{}, "")
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v5-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV5", []string{}, "")
vault.TestAddTestPlugin(t, cluster.Cores[0].Core, "mock-v6-database-plugin-muxed", consts.PluginTypeDatabase, "TestBackend_PluginMain_MockV6Multiplexed", []string{}, "")
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 database backend")
}
defer b.Cleanup(context.Background())
type testCase struct {
dbName string
dbType string
configData map[string]interface{}
assertDynamicUsername stringAssertion
assertDynamicPassword stringAssertion
}
tests := map[string]testCase{
"v4": {
dbName: "mockv4",
dbType: "mock-v4-database-plugin",
configData: map[string]interface{}{
"name": "mockv4",
"plugin_name": "mock-v4-database-plugin",
"connection_url": "sample_connection_url",
"verify_connection": true,
"allowed_roles": []string{"*"},
"username": "mockv4-user",
"password": "mysecurepassword",
},
assertDynamicUsername: assertStringPrefix("mockv4_user_"),
assertDynamicPassword: assertStringPrefix("mockv4_"),
},
"v5": {
dbName: "mockv5",
dbType: "mock-v5-database-plugin",
configData: map[string]interface{}{
"connection_url": "sample_connection_url",
"plugin_name": "mock-v5-database-plugin",
"verify_connection": true,
"allowed_roles": []string{"*"},
"name": "mockv5",
"username": "mockv5-user",
"password": "mysecurepassword",
},
assertDynamicUsername: assertStringPrefix("mockv5_user_"),
assertDynamicPassword: assertStringRegex("^[a-zA-Z0-9-]{20}"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var cleanupReqs []*logical.Request
defer func() {
// Do not defer cleanup directly so that we can populate the
// slice before the function gets executed.
cleanup(t, b, cleanupReqs)
}()
// /////////////////////////////////////////////////////////////////
// Configure
req := &logical.Request{
Operation: logical.CreateOperation,
Path: fmt.Sprintf("config/%s", test.dbName),
Storage: config.StorageView,
Data: test.configData,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertNoRespData(t, resp)
cleanupReqs = append(cleanupReqs, &logical.Request{
Operation: logical.DeleteOperation,
Path: fmt.Sprintf("config/%s", test.dbName),
Storage: config.StorageView,
})
// /////////////////////////////////////////////////////////////////
// Rotate root credentials
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: fmt.Sprintf("rotate-root/%s", test.dbName),
Storage: config.StorageView,
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err = b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertNoRespData(t, resp)
// /////////////////////////////////////////////////////////////////
// Dynamic credentials
// Create role
dynamicRoleName := "dynamic-role"
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: fmt.Sprintf("roles/%s", dynamicRoleName),
Storage: config.StorageView,
Data: map[string]interface{}{
"db_name": test.dbName,
"default_ttl": "5s",
"max_ttl": "1m",
},
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err = b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertNoRespData(t, resp)
cleanupReqs = append(cleanupReqs, &logical.Request{
Operation: logical.DeleteOperation,
Path: fmt.Sprintf("roles/%s", dynamicRoleName),
Storage: config.StorageView,
})
// Generate credentials
req = &logical.Request{
Operation: logical.ReadOperation,
Path: fmt.Sprintf("creds/%s", dynamicRoleName),
Storage: config.StorageView,
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err = b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertRespHasData(t, resp)
// TODO: Figure out how to make a call to the cluster that gives back a lease ID
// And also rotates the secret out after its TTL
// /////////////////////////////////////////////////////////////////
// Static credentials
// Create static role
staticRoleName := "static-role"
req = &logical.Request{
Operation: logical.CreateOperation,
Path: fmt.Sprintf("static-roles/%s", staticRoleName),
Storage: config.StorageView,
Data: map[string]interface{}{
"db_name": test.dbName,
"username": "static-username",
"rotation_period": "5",
},
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err = b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertNoRespData(t, resp)
cleanupReqs = append(cleanupReqs, &logical.Request{
Operation: logical.DeleteOperation,
Path: fmt.Sprintf("static-roles/%s", staticRoleName),
Storage: config.StorageView,
})
// Get credentials
req = &logical.Request{
Operation: logical.ReadOperation,
Path: fmt.Sprintf("static-creds/%s", staticRoleName),
Storage: config.StorageView,
}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err = b.HandleRequest(ctx, req)
assertErrIsNil(t, err)
assertRespHasNoErr(t, resp)
assertRespHasData(t, resp)
})
}
}
func cleanup(t *testing.T, b *databaseBackend, reqs []*logical.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Go in stack order so it works similar to defer
for i := len(reqs) - 1; i >= 0; i-- {
req := reqs[i]
resp, err := b.HandleRequest(ctx, req)
if err != nil {
t.Fatalf("Error cleaning up: %s", err)
}
if resp != nil && resp.IsError() {
t.Fatalf("Error cleaning up: %s", resp.Error())
}
}
}
func TestBackend_PluginMain_MockV4(t *testing.T) {
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
return
}
caPEM := os.Getenv(pluginutil.PluginCACertPEMEnv)
if caPEM == "" {
t.Fatal("CA cert not passed in")
}
args := []string{"--ca-cert=" + caPEM}
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(args)
RunV4(apiClientMeta.GetTLSConfig())
}
func TestBackend_PluginMain_MockV5(t *testing.T) {
if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" {
return
}
RunV5()
}
func TestBackend_PluginMain_MockV6Multiplexed(t *testing.T) {
if os.Getenv(pluginutil.PluginVaultVersionEnv) == "" {
return
}
RunV6Multiplexed()
}
func assertNoRespData(t *testing.T, resp *logical.Response) {
t.Helper()
if resp != nil && len(resp.Data) > 0 {
t.Fatalf("Response had data when none was expected: %#v", resp.Data)
}
}
func assertRespHasData(t *testing.T, resp *logical.Response) {
t.Helper()
if resp == nil || len(resp.Data) == 0 {
t.Fatalf("Response didn't have any data when some was expected")
}
}
type stringAssertion func(t *testing.T, str string)
func assertStringPrefix(expectedPrefix string) stringAssertion {
return func(t *testing.T, str string) {
t.Helper()
if !strings.HasPrefix(str, expectedPrefix) {
t.Fatalf("Missing prefix '%s': Actual: '%s'", expectedPrefix, str)
}
}
}
func assertStringRegex(expectedRegex string) stringAssertion {
re := regexp.MustCompile(expectedRegex)
return func(t *testing.T, str string) {
if !re.MatchString(str) {
t.Fatalf("Actual: '%s' did not match regexp '%s'", str, expectedRegex)
}
}
}
func assertRespHasNoErr(t *testing.T, resp *logical.Response) {
t.Helper()
if resp != nil && resp.IsError() {
t.Fatalf("response is error: %#v\n", resp)
}
}
func assertErrIsNil(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("No error expected, got: %s", err)
}
}