2021-04-16 21:52:35 +00:00
|
|
|
package cassandra
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
2021-06-21 17:38:08 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
2021-04-16 21:52:35 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gocql/gocql"
|
|
|
|
"github.com/hashicorp/vault/helper/testhelpers/cassandra"
|
|
|
|
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
2021-06-21 17:38:08 +00:00
|
|
|
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
|
|
"github.com/stretchr/testify/require"
|
2021-04-16 21:52:35 +00:00
|
|
|
)
|
|
|
|
|
2022-01-27 18:06:34 +00:00
|
|
|
var insecureFileMounts = map[string]string{
|
|
|
|
"test-fixtures/no_tls/cassandra.yaml": "/etc/cassandra/cassandra.yaml",
|
|
|
|
}
|
2021-04-16 21:52:35 +00:00
|
|
|
|
2021-06-21 17:38:08 +00:00
|
|
|
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"),
|
2021-08-12 02:11:12 +00:00
|
|
|
cassandra.Image("bitnami/cassandra", "3.11.11"),
|
2021-06-21 17:38:08 +00:00
|
|
|
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)
|
|
|
|
|
2021-04-16 21:52:35 +00:00
|
|
|
type testCase struct {
|
|
|
|
config map[string]interface{}
|
|
|
|
expectErr bool
|
|
|
|
}
|
|
|
|
|
2021-06-21 17:38:08 +00:00
|
|
|
caPEM := loadFile(t, "test-fixtures/with_tls/ca.pem")
|
|
|
|
badCAPEM := loadFile(t, "test-fixtures/with_tls/bad_ca.pem")
|
|
|
|
|
2021-04-16 21:52:35 +00:00
|
|
|
tests := map[string]testCase{
|
2021-06-21 17:38:08 +00:00
|
|
|
// ///////////////////////
|
|
|
|
// 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": "",
|
|
|
|
},
|
2021-04-16 21:52:35 +00:00
|
|
|
expectErr: true,
|
|
|
|
},
|
2021-06-21 17:38:08 +00:00
|
|
|
|
|
|
|
// ///////////////////////
|
|
|
|
// no cert data provided
|
|
|
|
"no cert data/tls=true": {
|
2021-04-16 21:52:35 +00:00
|
|
|
config: map[string]interface{}{
|
|
|
|
"tls": "true",
|
|
|
|
},
|
|
|
|
expectErr: true,
|
|
|
|
},
|
2021-06-21 17:38:08 +00:00
|
|
|
"no cert data/tls=false": {
|
|
|
|
config: map[string]interface{}{
|
|
|
|
"tls": "false",
|
|
|
|
},
|
|
|
|
expectErr: true,
|
|
|
|
},
|
|
|
|
"no cert data/insecure_tls": {
|
2021-04-16 21:52:35 +00:00
|
|
|
config: map[string]interface{}{
|
2021-06-21 17:38:08 +00:00
|
|
|
"insecure_tls": "true",
|
2021-04-16 21:52:35 +00:00
|
|
|
},
|
|
|
|
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{}{
|
2021-06-21 17:38:08 +00:00
|
|
|
"hosts": host.Name,
|
2021-04-16 21:52:35 +00:00
|
|
|
"port": host.Port,
|
|
|
|
"username": "cassandra",
|
|
|
|
"password": "cassandra",
|
2021-06-21 17:38:08 +00:00
|
|
|
"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",
|
2021-04-16 21:52:35 +00:00
|
|
|
}
|
2021-06-21 17:38:08 +00:00
|
|
|
|
|
|
|
// Apply the generated & common fields to the config to be sent to the DB
|
2021-04-16 21:52:35 +00:00
|
|
|
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)
|
|
|
|
}
|
2021-06-21 17:38:08 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2021-04-16 21:52:35 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-06-21 17:38:08 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|