blob: e2f4ba0fc59f6d78ed9eb2a6bbd13f13375337dd [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package cassandra
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"io/ioutil"
"testing"
"time"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/helper/testhelpers/cassandra"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/stretchr/testify/require"
)
var insecureFileMounts = map[string]string{
"test-fixtures/no_tls/cassandra.yaml": "/etc/cassandra/cassandra.yaml",
}
func TestSelfSignedCA(t *testing.T) {
copyFromTo := map[string]string{
"test-fixtures/with_tls/stores": "/bitnami/cassandra/secrets/",
"test-fixtures/with_tls/cqlshrc": "/.cassandra/cqlshrc",
}
tlsConfig := loadServerCA(t, "test-fixtures/with_tls/ca.pem")
// Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the
// ServerName is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI
// however by manually setting the ServerName we can get around the hostname/DNS issue and get them passing.
// Setting the ServerName isn't the ideal solution, but it was the only reliable one I was able to find
tlsConfig.ServerName = "cassandra"
sslOpts := &gocql.SslOptions{
Config: tlsConfig,
EnableHostVerification: true,
}
host, cleanup := cassandra.PrepareTestContainer(t,
cassandra.ContainerName("cassandra"),
cassandra.Image("bitnami/cassandra", "3.11.11"),
cassandra.CopyFromTo(copyFromTo),
cassandra.SslOpts(sslOpts),
cassandra.Env("CASSANDRA_KEYSTORE_PASSWORD=cassandra"),
cassandra.Env("CASSANDRA_TRUSTSTORE_PASSWORD=cassandra"),
cassandra.Env("CASSANDRA_INTERNODE_ENCRYPTION=none"),
cassandra.Env("CASSANDRA_CLIENT_ENCRYPTION=true"),
)
t.Cleanup(cleanup)
type testCase struct {
config map[string]interface{}
expectErr bool
}
caPEM := loadFile(t, "test-fixtures/with_tls/ca.pem")
badCAPEM := loadFile(t, "test-fixtures/with_tls/bad_ca.pem")
tests := map[string]testCase{
// ///////////////////////
// pem_json tests
"pem_json/ca only": {
config: map[string]interface{}{
"pem_json": toJSON(t, certutil.CertBundle{
CAChain: []string{caPEM},
}),
},
expectErr: false,
},
"pem_json/bad ca": {
config: map[string]interface{}{
"pem_json": toJSON(t, certutil.CertBundle{
CAChain: []string{badCAPEM},
}),
},
expectErr: true,
},
"pem_json/missing ca": {
config: map[string]interface{}{
"pem_json": "",
},
expectErr: true,
},
// ///////////////////////
// pem_bundle tests
"pem_bundle/ca only": {
config: map[string]interface{}{
"pem_bundle": caPEM,
},
expectErr: false,
},
"pem_bundle/unrecognized CA": {
config: map[string]interface{}{
"pem_bundle": badCAPEM,
},
expectErr: true,
},
"pem_bundle/missing ca": {
config: map[string]interface{}{
"pem_bundle": "",
},
expectErr: true,
},
// ///////////////////////
// no cert data provided
"no cert data/tls=true": {
config: map[string]interface{}{
"tls": "true",
},
expectErr: true,
},
"no cert data/tls=false": {
config: map[string]interface{}{
"tls": "false",
},
expectErr: true,
},
"no cert data/insecure_tls": {
config: map[string]interface{}{
"insecure_tls": "true",
},
expectErr: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Set values that we don't know until the cassandra container is started
config := map[string]interface{}{
"hosts": host.Name,
"port": host.Port,
"username": "cassandra",
"password": "cassandra",
"protocol_version": "4",
"connect_timeout": "30s",
"tls": "true",
// Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the
// tls_server_name is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI
// however by manually setting the tls_server_name we can get around the hostname/DNS issue and get them passing.
// Setting the tls_server_name isn't the ideal solution, but it was the only reliable one I was able to find
"tls_server_name": "cassandra",
}
// Apply the generated & common fields to the config to be sent to the DB
for k, v := range test.config {
config[k] = v
}
db := new()
initReq := dbplugin.InitializeRequest{
Config: config,
VerifyConnection: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.Initialize(ctx, initReq)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
// If no error expected, run a NewUser query to make sure the connection
// actually works in case Initialize doesn't catch it
if !test.expectErr {
assertNewUser(t, db, sslOpts)
}
})
}
}
func assertNewUser(t *testing.T, db *Cassandra, sslOpts *gocql.SslOptions) {
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "dispname",
RoleName: "rolename",
},
Statements: dbplugin.Statements{
Commands: []string{
"create user '{{username}}' with password '{{password}}'",
},
},
RollbackStatements: dbplugin.Statements{},
Password: "gh8eruajASDFAsgy89svn",
Expiration: time.Now().Add(5 * time.Second),
}
newUserResp := dbtesting.AssertNewUser(t, db, newUserReq)
t.Logf("Username: %s", newUserResp.Username)
assertCreds(t, db.Hosts, db.Port, newUserResp.Username, newUserReq.Password, sslOpts, 5*time.Second)
}
func loadServerCA(t *testing.T, file string) *tls.Config {
t.Helper()
pemData, err := ioutil.ReadFile(file)
require.NoError(t, err)
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pemData)
config := &tls.Config{
RootCAs: pool,
}
return config
}
func loadFile(t *testing.T, filename string) string {
t.Helper()
contents, err := ioutil.ReadFile(filename)
require.NoError(t, err)
return string(contents)
}
func toJSON(t *testing.T, val interface{}) string {
t.Helper()
b, err := json.Marshal(val)
require.NoError(t, err)
return string(b)
}