Files
threshold/internal/credential/vault/supported.go
T
Johan Brandhorst-Satzkorn 7f9b294a7e refact(all): Use testing.TB for test helpers
Our test helper packages were not possible to use
with benchmarks. This refactor switches to testing.TB
to allow both tests and benchmarks to use the helpers.
2022-04-29 16:33:44 -04:00

369 lines
10 KiB
Go

//go:build linux || darwin || windows
// +build linux darwin windows
package vault
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/hashicorp/boundary/internal/db/common"
"github.com/hashicorp/go-rootcerts"
vault "github.com/hashicorp/vault/api"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/ory/dockertest/v3"
"github.com/stretchr/testify/require"
)
const DefaultVaultVersion = "1.7.2"
func init() {
newVaultServer = gotNewServer
mountDatabase = gotMountDatabase
}
func gotDocker(t testing.TB) {}
func gotNewServer(t testing.TB, opt ...TestOption) *TestVaultServer {
const (
serverTlsTemplate = `{
"listener": [
{
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": "false",
"tls_cert_file": "/vault/config/certificates/certificate.pem",
"tls_key_file": "/vault/config/certificates/key.pem"
}
}
]
}
`
clientTlsTemplate = `{
"listener": [
{
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": "false",
"tls_cert_file": "/vault/config/certificates/certificate.pem",
"tls_key_file": "/vault/config/certificates/key.pem",
"tls_require_and_verify_client_cert": "true",
"tls_client_ca_file": "/vault/config/certificates/client-ca-certificate.pem"
}
}
]
}
`
)
require := require.New(t)
pool, err := dockertest.NewPool("")
require.NoError(err)
opts := getTestOpts(t, opt...)
server := &TestVaultServer{
RootToken: fmt.Sprintf("icu-root-%s", t.Name()),
pool: pool,
}
dockerOptions := &dockertest.RunOptions{
Repository: "vault",
Tag: DefaultVaultVersion,
Env: []string{fmt.Sprintf("VAULT_DEV_ROOT_TOKEN_ID=%s", server.RootToken)},
}
vConfig := vault.DefaultConfig()
if opts.vaultTLS != TestNoTLS {
dockerOptions.Env = append(dockerOptions.Env, "VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8300")
switch opts.vaultTLS {
case TestServerTLS:
dockerOptions.Env = append(dockerOptions.Env, fmt.Sprintf("VAULT_LOCAL_CONFIG=%s", serverTlsTemplate))
case TestClientTLS:
dockerOptions.Env = append(dockerOptions.Env, fmt.Sprintf("VAULT_LOCAL_CONFIG=%s", clientTlsTemplate))
}
serverCert := testServerCert(t, testCaCert(t), "localhost")
server.serverCertBundle = serverCert
server.ServerCert = serverCert.Cert.Cert
server.CaCert = serverCert.CA.Cert
clientTLSConfig := vConfig.HttpClient.Transport.(*http.Transport).TLSClientConfig
rootConfig := &rootcerts.Config{
CACertificate: serverCert.CA.Cert,
}
require.NoError(rootcerts.ConfigureTLS(clientTLSConfig, rootConfig))
dataSrcDir := t.TempDir()
require.NoError(os.Chmod(dataSrcDir, 0o777))
caCertFn := filepath.Join(dataSrcDir, "ca-certificate.pem")
require.NoError(ioutil.WriteFile(caCertFn, serverCert.CA.Cert, 0o777))
certFn := filepath.Join(dataSrcDir, "certificate.pem")
require.NoError(ioutil.WriteFile(certFn, serverCert.Cert.Cert, 0o777))
keyFn := filepath.Join(dataSrcDir, "key.pem")
require.NoError(ioutil.WriteFile(keyFn, serverCert.Cert.Key, 0o777))
dockerOptions.Mounts = append(dockerOptions.Mounts, fmt.Sprintf("%s:/vault/config/certificates", dataSrcDir))
if opts.vaultTLS == TestClientTLS {
clientCert := testClientCert(t, testCaCert(t), opt...)
server.clientCertBundle = clientCert
server.ClientCert = clientCert.Cert.Cert
server.ClientKey = clientCert.Cert.Key
clientCaCertFn := filepath.Join(dataSrcDir, "client-ca-certificate.pem")
require.NoError(ioutil.WriteFile(clientCaCertFn, clientCert.CA.Cert, 0o777))
vaultClientCert, err := tls.X509KeyPair(server.ClientCert, server.ClientKey)
require.NoError(err)
clientTLSConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &vaultClientCert, nil
}
}
}
// NOTE(mgaffney) 05/2021: creating a docker network is not the default
// because it added a significant amount time to the tests.
//
// For reference, running 'go test .'
// - without creating a docker network by default: 259.668s
// - with creating a docker network by default: 553.497s
//
// Machine: MacBook Pro (15-inch, 2018)
// Processor: 2.6 GHz 6-Core Intel Core i7
// Memory: 16 GB 2400 MHz DDR4
// OS: 10.15.7 (Catalina)
//
// Docker
// Desktop: 3.3.3 (64133)
// Engine: 20.10.6
if opts.dockerNetwork {
network, err := pool.CreateNetwork(t.Name())
require.NoError(err)
server.network = network
dockerOptions.Networks = []*dockertest.Network{network}
if !opts.skipCleanup {
t.Cleanup(func() {
network.Close()
})
}
}
resource, err := pool.RunWithOptions(dockerOptions)
require.NoError(err)
if !opts.skipCleanup {
t.Cleanup(func() {
cleanupResource(t, pool, resource)
})
}
server.vaultContainer = resource
switch opts.vaultTLS {
case TestNoTLS:
server.Addr = fmt.Sprintf("http://localhost:%s", resource.GetPort("8200/tcp"))
case TestServerTLS, TestClientTLS:
server.Addr = fmt.Sprintf("https://localhost:%s", resource.GetPort("8200/tcp"))
default:
t.Fatal("unknown TLS option")
}
vConfig.Address = server.Addr
client, err := vault.NewClient(vConfig)
require.NoError(err)
client.SetToken(server.RootToken)
err = pool.Retry(func() error {
if _, err := client.Sys().Health(); err != nil {
return err
}
return nil
})
require.NoError(err)
server.addPolicy(t, "boundary-controller", requiredCapabilities)
return server
}
func gotMountDatabase(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestDatabase {
require := require.New(t)
require.Nil(v.postgresContainer, "postgres container exists")
require.NotNil(v.network, "Vault server must be created with docker network")
pool, ok := v.pool.(*dockertest.Pool)
require.True(ok)
network, ok := v.network.(*dockertest.Network)
require.True(ok)
dockerOptions := &dockertest.RunOptions{
Repository: "postgres",
Tag: "11",
Networks: []*dockertest.Network{network},
Env: []string{"POSTGRES_PASSWORD=password", "POSTGRES_DB=boundarytest"},
}
opts := getTestOpts(t, opt...)
resource, err := pool.RunWithOptions(dockerOptions)
require.NoError(err)
if !opts.skipCleanup {
t.Cleanup(func() {
cleanupResource(t, pool, resource)
})
}
v.postgresContainer = resource
dbUrlTemplate := fmt.Sprintf("postgres://%%s:%%s@%s/boundarytest?sslmode=disable", resource.GetHostPort("5432/tcp"))
dburl := fmt.Sprintf(dbUrlTemplate, "postgres", "password")
err = pool.Retry(func() error {
var err error
db, err := common.SqlOpen("postgres", dburl)
if err != nil {
return err
}
return db.Ping()
})
require.NoError(err)
const (
createOpened = `create table boundary_opened ( name text primary key )`
createClosed = `create table boundary_closed ( name text primary key )`
createVaultAccount = `create role vault with superuser login createrole password 'vault-password'`
createOpenedRole = `create role opened_role noinherit`
createClosedRole = `create role closed_role noinherit`
grantOpenedRole = `grant select, insert, update, delete on boundary_opened to opened_role`
grantClosedRole = `grant select, insert, update, delete on boundary_closed to closed_role`
)
db, err := common.SqlOpen("postgres", dburl)
require.NoError(err)
defer db.Close()
exec := func(q string) {
_, err := db.Exec(q)
require.NoError(err, q)
}
exec(createOpened)
exec(createClosed)
exec(createVaultAccount)
exec(createOpenedRole)
exec(createClosedRole)
exec(grantOpenedRole)
exec(grantClosedRole)
vc := v.client(t).cl
// Mount Database
maxTTL := 24 * time.Hour
if t, ok := t.(*testing.T); ok {
if deadline, ok := t.Deadline(); ok {
maxTTL = time.Until(deadline) * 2
}
}
defaultTTL := maxTTL / 2
t.Logf("maxTTL: %s, defaultTTL: %s", maxTTL, defaultTTL)
mountInput := &vault.MountInput{
Type: "database",
Description: t.Name(),
Config: vault.MountConfigInput{
DefaultLeaseTTL: defaultTTL.String(),
MaxLeaseTTL: maxTTL.String(),
},
}
mountPath := opts.mountPath
if mountPath == "" {
mountPath = "database/"
}
require.NoError(vc.Sys().Mount(mountPath, mountInput))
policyPath := fmt.Sprintf("%s*", mountPath)
pc := pathCapabilities{
policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability,
}
v.addPolicy(t, "database", pc)
vaultContainer, ok := v.vaultContainer.(*dockertest.Resource)
require.True(ok)
t.Log(vaultContainer.GetIPInNetwork(network))
// Configure PostgreSQL secrets engine
connUrl := fmt.Sprintf("postgresql://{{username}}:{{password}}@%s:5432/boundarytest?sslmode=disable", resource.GetIPInNetwork(network))
t.Log(connUrl)
postgresConfPath := path.Join(mountPath, "config/postgresql")
postgresConfOptions := map[string]interface{}{
"plugin_name": "postgresql-database-plugin",
"connection_url": connUrl,
"allowed_roles": "opened,closed",
"username": "vault",
"password": "vault-password",
}
s, err := vc.Logical().Write(postgresConfPath, postgresConfOptions)
require.NoError(err)
require.NotEmpty(s)
const (
vaultOpenedCreationStatement = `
create role "{{name}}"
with login password '{{password}}'
valid until '{{expiration}}' inherit;
grant opened_role to "{{name}}";
`
vaultClosedCreationStatement = `
create role "{{name}}"
with login password '{{password}}'
valid until '{{expiration}}' inherit;
grant closed_role to "{{name}}";
`
)
openedRolePath := path.Join(mountPath, "roles", "opened")
openedRoleOptions := map[string]interface{}{
"db_name": "postgresql",
"creation_statements": vaultOpenedCreationStatement,
}
_, err = vc.Logical().Write(openedRolePath, openedRoleOptions)
require.NoError(err)
closedRolePath := path.Join(mountPath, "roles", "closed")
closedRoleOptions := map[string]interface{}{
"db_name": "postgresql",
"creation_statements": vaultClosedCreationStatement,
}
_, err = vc.Logical().Write(closedRolePath, closedRoleOptions)
require.NoError(err)
return &TestDatabase{
URL: TestDatabaseURL(dbUrlTemplate),
}
}
func cleanupResource(t testing.TB, pool *dockertest.Pool, resource *dockertest.Resource) {
t.Helper()
var err error
for i := 0; i < 10; i++ {
err = pool.Purge(resource)
if err == nil {
return
}
time.Sleep(1 * time.Second)
}
if strings.Contains(err.Error(), "No such container") {
return
}
t.Fatalf("Failed to cleanup local container: %s", err)
}