// 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) }