1
0

remove cassandra

This commit is contained in:
Konstantin Demin 2024-07-01 11:30:10 +03:00
parent 37d5afd3ed
commit afabbdd1ce
24 changed files with 2 additions and 3026 deletions

View File

@ -10,7 +10,6 @@
/builtin/credential/okta/ @hashicorp/vault-ecosystem
# Secrets engines (pki, ssh, totp and transit omitted)
/builtin/logical/cassandra/ @hashicorp/vault-ecosystem
/builtin/logical/consul/ @hashicorp/vault-ecosystem
/builtin/logical/database/ @hashicorp/vault-ecosystem
/builtin/logical/mysql/ @hashicorp/vault-ecosystem

View File

@ -307,9 +307,6 @@ mysql-database-plugin:
mysql-legacy-database-plugin:
@CGO_ENABLED=0 $(GO_CMD) build -o bin/mysql-legacy-database-plugin ./plugins/database/mysql/mysql-legacy-database-plugin
cassandra-database-plugin:
@CGO_ENABLED=0 $(GO_CMD) build -o bin/cassandra-database-plugin ./plugins/database/cassandra/cassandra-database-plugin
influxdb-database-plugin:
@CGO_ENABLED=0 $(GO_CMD) build -o bin/influxdb-database-plugin ./plugins/database/influxdb/influxdb-database-plugin
@ -366,7 +363,7 @@ ci-copywriteheaders:
cd sdk && $(CURDIR)/scripts/copywrite-exceptions.sh
cd shamir && $(CURDIR)/scripts/copywrite-exceptions.sh
.PHONY: all bin default prep test vet bootstrap fmt fmtcheck mysql-database-plugin mysql-legacy-database-plugin cassandra-database-plugin influxdb-database-plugin postgresql-database-plugin ember-dist ember-dist-dev static-dist static-dist-dev assetcheck check-vault-in-path packages build build-ci semgrep semgrep-ci vet-codechecker ci-vet-codechecker clean dev
.PHONY: all bin default prep test vet bootstrap fmt fmtcheck mysql-database-plugin mysql-legacy-database-plugin influxdb-database-plugin postgresql-database-plugin ember-dist ember-dist-dev static-dist static-dist-dev assetcheck check-vault-in-path packages build build-ci semgrep semgrep-ci vet-codechecker ci-vet-codechecker clean dev
.NOTPARALLEL: ember-dist ember-dist-dev

View File

@ -42,7 +42,6 @@ import (
logicalDb "github.com/hashicorp/vault/builtin/logical/database"
physAerospike "github.com/hashicorp/vault/physical/aerospike"
physCassandra "github.com/hashicorp/vault/physical/cassandra"
physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb"
physConsul "github.com/hashicorp/vault/physical/consul"
physFoundationDB "github.com/hashicorp/vault/physical/foundationdb"
@ -171,7 +170,6 @@ var (
physicalBackends = map[string]physical.Factory{
"aerospike": physAerospike.NewAerospikeBackend,
"cassandra": physCassandra.NewCassandraBackend,
"cockroachdb": physCockroachDB.NewCockroachDBBackend,
"consul": physConsul.NewConsulBackend,
"file_transactional": physFile.NewTransactionalFileBackend,

4
go.mod
View File

@ -32,7 +32,6 @@ require (
github.com/armon/go-radix v1.0.0
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a
github.com/cenkalti/backoff/v3 v3.2.2
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
@ -47,7 +46,6 @@ require (
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-sql-driver/mysql v1.6.0
github.com/go-test/deep v1.1.0
github.com/gocql/gocql v1.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/protobuf v1.5.3
github.com/google/go-cmp v0.6.0
@ -215,6 +213,7 @@ require (
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/centrify/cloud-golang-sdk v0.0.0-20210923165758-a8c48d049166 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@ -271,7 +270,6 @@ require (
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gophercloud/gophercloud v0.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/cronexpr v1.1.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.0.0 // indirect

7
go.sum
View File

@ -1006,15 +1006,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@ -1636,8 +1633,6 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c=
github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@ -1851,8 +1846,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp-forge/bbolt v1.3.8-hc3 h1:iTWR3RDPj0TGChAvJ8QjHFcNFWAUVgNQV73IE6gAX4E=
github.com/hashicorp-forge/bbolt v1.3.8-hc3/go.mod h1:sQBu5UIJ+rcUFU4Fo9rpTHNV935jwmGWS3dQ/MV8810=
github.com/hashicorp/cap v0.3.0 h1:zFzVxuWy78lO6QRLHu/ONkjx/Jh0lpfvPgmpDGri43E=

View File

@ -31,7 +31,6 @@ import (
logicalSsh "github.com/hashicorp/vault/builtin/logical/ssh"
logicalTotp "github.com/hashicorp/vault/builtin/logical/totp"
logicalTransit "github.com/hashicorp/vault/builtin/logical/transit"
dbCass "github.com/hashicorp/vault/plugins/database/cassandra"
dbInflux "github.com/hashicorp/vault/plugins/database/influxdb"
dbMysql "github.com/hashicorp/vault/plugins/database/mysql"
dbPostgres "github.com/hashicorp/vault/plugins/database/postgresql"
@ -110,7 +109,6 @@ func newRegistry() *registry {
"mysql-rds-database-plugin": {Factory: dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate)},
"mysql-legacy-database-plugin": {Factory: dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate)},
"cassandra-database-plugin": {Factory: dbCass.New},
"influxdb-database-plugin": {Factory: dbInflux.New},
"postgresql-database-plugin": {Factory: dbPostgres.New},
},
@ -119,10 +117,6 @@ func newRegistry() *registry {
Factory: logicalAd.Factory,
DeprecationStatus: consts.Deprecated,
},
"cassandra": {
Factory: removedFactory,
DeprecationStatus: consts.Removed,
},
"consul": {Factory: logicalConsul.Factory},
"kubernetes": {Factory: logicalKube.Factory},
"kv": {Factory: logicalKv.Factory},

View File

@ -1,178 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/sdk/helper/docker"
)
type containerConfig struct {
containerName string
imageName string
version string
copyFromTo map[string]string
env []string
sslOpts *gocql.SslOptions
}
type ContainerOpt func(*containerConfig)
func ContainerName(name string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.containerName = name
}
}
func Image(imageName string, version string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.imageName = imageName
cfg.version = version
// Reset the environment because there's a very good chance the default environment doesn't apply to the
// non-default image being used
cfg.env = nil
}
}
func Version(version string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.version = version
}
}
func CopyFromTo(copyFromTo map[string]string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.copyFromTo = copyFromTo
}
}
func Env(keyValue string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.env = append(cfg.env, keyValue)
}
}
func SslOpts(sslOpts *gocql.SslOptions) ContainerOpt {
return func(cfg *containerConfig) {
cfg.sslOpts = sslOpts
}
}
type Host struct {
Name string
Port string
}
func (h Host) ConnectionURL() string {
return net.JoinHostPort(h.Name, h.Port)
}
func PrepareTestContainer(t *testing.T, opts ...ContainerOpt) (Host, func()) {
t.Helper()
if os.Getenv("CASSANDRA_HOSTS") != "" {
host, port, err := net.SplitHostPort(os.Getenv("CASSANDRA_HOSTS"))
if err != nil {
t.Fatalf("Failed to split host & port from CASSANDRA_HOSTS (%s): %s", os.Getenv("CASSANDRA_HOSTS"), err)
}
h := Host{
Name: host,
Port: port,
}
return h, func() {}
}
containerCfg := &containerConfig{
imageName: "docker.mirror.hashicorp.services/library/cassandra",
containerName: "cassandra",
version: "3.11",
env: []string{"CASSANDRA_BROADCAST_ADDRESS=127.0.0.1"},
}
for _, opt := range opts {
opt(containerCfg)
}
copyFromTo := map[string]string{}
for from, to := range containerCfg.copyFromTo {
absFrom, err := filepath.Abs(from)
if err != nil {
t.Fatalf("Unable to get absolute path for file %s", from)
}
copyFromTo[absFrom] = to
}
runOpts := docker.RunOptions{
ContainerName: containerCfg.containerName,
ImageRepo: containerCfg.imageName,
ImageTag: containerCfg.version,
Ports: []string{"9042/tcp"},
CopyFromTo: copyFromTo,
Env: containerCfg.env,
}
runner, err := docker.NewServiceRunner(runOpts)
if err != nil {
t.Fatalf("Could not start docker cassandra: %s", err)
}
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
cfg := docker.NewServiceHostPort(host, port)
clusterConfig := gocql.NewCluster(cfg.Address())
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
Username: "cassandra",
Password: "cassandra",
}
clusterConfig.Timeout = 30 * time.Second
clusterConfig.ProtoVersion = 4
clusterConfig.Port = port
clusterConfig.SslOpts = containerCfg.sslOpts
session, err := clusterConfig.CreateSession()
if err != nil {
return nil, fmt.Errorf("error creating session: %s", err)
}
defer session.Close()
// Create keyspace
query := session.Query(`CREATE KEYSPACE "vault" WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };`)
if err := query.Exec(); err != nil {
t.Fatalf("could not create cassandra keyspace: %v", err)
}
// Create table
query = session.Query(`CREATE TABLE "vault"."entries" (
bucket text,
key text,
value blob,
PRIMARY KEY (bucket, key)
) WITH CLUSTERING ORDER BY (key ASC);`)
if err := query.Exec(); err != nil {
t.Fatalf("could not create cassandra table: %v", err)
}
return cfg, nil
})
if err != nil {
t.Fatalf("Could not start docker cassandra: %s", err)
}
host, port, err := net.SplitHostPort(svc.Config.Address())
if err != nil {
t.Fatalf("Failed to split host & port from address (%s): %s", svc.Config.Address(), err)
}
h := Host{
Name: host,
Port: port,
}
return h, svc.Cleanup
}

View File

@ -155,7 +155,6 @@ func (m *mockBuiltinRegistry) Keys(pluginType consts.PluginType) []string {
"mysql-rds-database-plugin",
"mysql-legacy-database-plugin",
"cassandra-database-plugin",
"influxdb-database-plugin",
"postgresql-database-plugin",
}

View File

@ -1,366 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"strconv"
"strings"
"time"
metrics "github.com/armon/go-metrics"
"github.com/gocql/gocql"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/physical"
)
// CassandraBackend is a physical backend that stores data in Cassandra.
type CassandraBackend struct {
sess *gocql.Session
table string
logger log.Logger
}
// Verify CassandraBackend satisfies the correct interfaces
var _ physical.Backend = (*CassandraBackend)(nil)
// NewCassandraBackend constructs a Cassandra backend using a pre-existing
// keyspace and table.
func NewCassandraBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
splitArray := func(v string) []string {
return strings.FieldsFunc(v, func(r rune) bool {
return r == ','
})
}
var (
hosts = splitArray(conf["hosts"])
port = 9042
explicitPort = false
keyspace = conf["keyspace"]
table = conf["table"]
consistency = gocql.LocalQuorum
)
if len(hosts) == 0 {
hosts = []string{"localhost"}
}
for i, hp := range hosts {
h, ps, err := net.SplitHostPort(hp)
if err != nil {
continue
}
p, err := strconv.Atoi(ps)
if err != nil {
return nil, err
}
if explicitPort && p != port {
return nil, fmt.Errorf("all hosts must have the same port")
}
hosts[i], port = h, p
explicitPort = true
}
if keyspace == "" {
keyspace = "vault"
}
if table == "" {
table = "entries"
}
if cs, ok := conf["consistency"]; ok {
switch cs {
case "ANY":
consistency = gocql.Any
case "ONE":
consistency = gocql.One
case "TWO":
consistency = gocql.Two
case "THREE":
consistency = gocql.Three
case "QUORUM":
consistency = gocql.Quorum
case "ALL":
consistency = gocql.All
case "LOCAL_QUORUM":
consistency = gocql.LocalQuorum
case "EACH_QUORUM":
consistency = gocql.EachQuorum
case "LOCAL_ONE":
consistency = gocql.LocalOne
default:
return nil, fmt.Errorf("'consistency' must be one of {ANY, ONE, TWO, THREE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM, LOCAL_ONE}")
}
}
connectStart := time.Now()
cluster := gocql.NewCluster(hosts...)
cluster.Port = port
cluster.Keyspace = keyspace
if retryCountStr, ok := conf["simple_retry_policy_retries"]; ok {
retryCount, err := strconv.Atoi(retryCountStr)
if err != nil || retryCount <= 0 {
return nil, fmt.Errorf("'simple_retry_policy_retries' must be a positive integer")
}
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: retryCount}
}
cluster.ProtoVersion = 2
if protoVersionStr, ok := conf["protocol_version"]; ok {
protoVersion, err := strconv.Atoi(protoVersionStr)
if err != nil {
return nil, fmt.Errorf("'protocol_version' must be an integer")
}
cluster.ProtoVersion = protoVersion
}
if username, ok := conf["username"]; ok {
if cluster.ProtoVersion < 2 {
return nil, fmt.Errorf("authentication is not supported with protocol version < 2")
}
authenticator := gocql.PasswordAuthenticator{Username: username}
if password, ok := conf["password"]; ok {
authenticator.Password = password
}
cluster.Authenticator = authenticator
}
if initialConnectionTimeoutStr, ok := conf["initial_connection_timeout"]; ok {
initialConnectionTimeout, err := strconv.Atoi(initialConnectionTimeoutStr)
if err != nil || initialConnectionTimeout <= 0 {
return nil, fmt.Errorf("'initial_connection_timeout' must be a positive integer")
}
cluster.ConnectTimeout = time.Duration(initialConnectionTimeout) * time.Second
}
if connTimeoutStr, ok := conf["connection_timeout"]; ok {
connectionTimeout, err := strconv.Atoi(connTimeoutStr)
if err != nil || connectionTimeout <= 0 {
return nil, fmt.Errorf("'connection_timeout' must be a positive integer")
}
cluster.Timeout = time.Duration(connectionTimeout) * time.Second
}
if err := setupCassandraTLS(conf, cluster); err != nil {
return nil, err
}
sess, err := cluster.CreateSession()
if err != nil {
return nil, err
}
metrics.MeasureSince([]string{"cassandra", "connect"}, connectStart)
sess.SetConsistency(consistency)
impl := &CassandraBackend{
sess: sess,
table: table,
logger: logger,
}
return impl, nil
}
func setupCassandraTLS(conf map[string]string, cluster *gocql.ClusterConfig) error {
tlsOnStr, ok := conf["tls"]
if !ok {
return nil
}
tlsOn, err := strconv.Atoi(tlsOnStr)
if err != nil {
return fmt.Errorf("'tls' must be an integer (0 or 1)")
}
if tlsOn == 0 {
return nil
}
tlsConfig := &tls.Config{}
if pemBundlePath, ok := conf["pem_bundle_file"]; ok {
pemBundleData, err := ioutil.ReadFile(pemBundlePath)
if err != nil {
return fmt.Errorf("error reading pem bundle from %q: %w", pemBundlePath, err)
}
pemBundle, err := certutil.ParsePEMBundle(string(pemBundleData))
if err != nil {
return fmt.Errorf("error parsing 'pem_bundle': %w", err)
}
tlsConfig, err = pemBundle.GetTLSConfig(certutil.TLSClient)
if err != nil {
return err
}
} else if pemJSONPath, ok := conf["pem_json_file"]; ok {
pemJSONData, err := ioutil.ReadFile(pemJSONPath)
if err != nil {
return fmt.Errorf("error reading json bundle from %q: %w", pemJSONPath, err)
}
pemJSON, err := certutil.ParsePKIJSON([]byte(pemJSONData))
if err != nil {
return err
}
tlsConfig, err = pemJSON.GetTLSConfig(certutil.TLSClient)
if err != nil {
return err
}
}
if tlsSkipVerifyStr, ok := conf["tls_skip_verify"]; ok {
tlsSkipVerify, err := strconv.Atoi(tlsSkipVerifyStr)
if err != nil {
return fmt.Errorf("'tls_skip_verify' must be an integer (0 or 1)")
}
if tlsSkipVerify == 0 {
tlsConfig.InsecureSkipVerify = false
} else {
tlsConfig.InsecureSkipVerify = true
}
}
if tlsMinVersion, ok := conf["tls_min_version"]; ok {
switch tlsMinVersion {
case "tls10":
tlsConfig.MinVersion = tls.VersionTLS10
case "tls11":
tlsConfig.MinVersion = tls.VersionTLS11
case "tls12":
tlsConfig.MinVersion = tls.VersionTLS12
case "tls13":
tlsConfig.MinVersion = tls.VersionTLS13
default:
return fmt.Errorf("'tls_min_version' must be one of `tls10`, `tls11`, `tls12` or `tls13`")
}
}
cluster.SslOpts = &gocql.SslOptions{
Config: tlsConfig,
EnableHostVerification: !tlsConfig.InsecureSkipVerify,
}
return nil
}
// bucketName sanitises a bucket name for Cassandra
func (c *CassandraBackend) bucketName(name string) string {
if name == "" {
name = "."
}
return strings.TrimRight(name, "/")
}
// bucket returns all the prefix buckets the key should be stored at
func (c *CassandraBackend) buckets(key string) []string {
vals := append([]string{""}, physical.Prefixes(key)...)
for i, v := range vals {
vals[i] = c.bucketName(v)
}
return vals
}
// bucket returns the most specific bucket for the key
func (c *CassandraBackend) bucket(key string) string {
bs := c.buckets(key)
return bs[len(bs)-1]
}
// Put is used to insert or update an entry
func (c *CassandraBackend) Put(ctx context.Context, entry *physical.Entry) error {
defer metrics.MeasureSince([]string{"cassandra", "put"}, time.Now())
// Execute inserts to each key prefix simultaneously
stmt := fmt.Sprintf(`INSERT INTO "%s" (bucket, key, value) VALUES (?, ?, ?)`, c.table)
buckets := c.buckets(entry.Key)
results := make(chan error, len(buckets))
for i, _bucket := range buckets {
go func(i int, bucket string) {
var value []byte
if i == len(buckets)-1 {
// Only store the full value if this is the leaf bucket where the entry will actually be read
// otherwise this write is just to allow for list operations
value = entry.Value
}
results <- c.sess.Query(stmt, bucket, entry.Key, value).Exec()
}(i, _bucket)
}
for i := 0; i < len(buckets); i++ {
if err := <-results; err != nil {
return err
}
}
return nil
}
// Get is used to fetch an entry
func (c *CassandraBackend) Get(ctx context.Context, key string) (*physical.Entry, error) {
defer metrics.MeasureSince([]string{"cassandra", "get"}, time.Now())
v := []byte(nil)
stmt := fmt.Sprintf(`SELECT value FROM "%s" WHERE bucket = ? AND key = ? LIMIT 1`, c.table)
q := c.sess.Query(stmt, c.bucket(key), key)
if err := q.Scan(&v); err != nil {
if err == gocql.ErrNotFound {
return nil, nil
}
return nil, err
}
return &physical.Entry{
Key: key,
Value: v,
}, nil
}
// Delete is used to permanently delete an entry
func (c *CassandraBackend) Delete(ctx context.Context, key string) error {
defer metrics.MeasureSince([]string{"cassandra", "delete"}, time.Now())
stmt := fmt.Sprintf(`DELETE FROM "%s" WHERE bucket = ? AND key = ?`, c.table)
buckets := c.buckets(key)
results := make(chan error, len(buckets))
for _, bucket := range buckets {
go func(bucket string) {
results <- c.sess.Query(stmt, bucket, key).Exec()
}(bucket)
}
for i := 0; i < len(buckets); i++ {
if err := <-results; err != nil {
return err
}
}
return nil
}
// List is used ot list all the keys under a given
// prefix, up to the next prefix.
func (c *CassandraBackend) List(ctx context.Context, prefix string) ([]string, error) {
defer metrics.MeasureSince([]string{"cassandra", "list"}, time.Now())
stmt := fmt.Sprintf(`SELECT key FROM "%s" WHERE bucket = ?`, c.table)
q := c.sess.Query(stmt, c.bucketName(prefix))
iter := q.Iter()
k, keys := "", []string{}
for iter.Scan(&k) {
// Only return the next "component" (with a trailing slash if it has children)
k = strings.TrimPrefix(k, prefix)
if parts := strings.SplitN(k, "/", 2); len(parts) > 1 {
k = parts[0] + "/"
} else {
k = parts[0]
}
// Deduplicate; this works because the keys are sorted
if len(keys) > 0 && keys[len(keys)-1] == k {
continue
}
keys = append(keys, k)
}
return keys, iter.Close()
}

View File

@ -1,60 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"os"
"reflect"
"testing"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/testhelpers/cassandra"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/physical"
)
func TestCassandraBackend(t *testing.T) {
if testing.Short() {
t.Skipf("skipping in short mode")
}
if os.Getenv("VAULT_CI_GO_TEST_RACE") != "" {
t.Skip("skipping race test in CI pending https://github.com/gocql/gocql/pull/1474")
}
host, cleanup := cassandra.PrepareTestContainer(t)
defer cleanup()
// Run vault tests
logger := logging.NewVaultLogger(log.Debug)
b, err := NewCassandraBackend(map[string]string{
"hosts": host.ConnectionURL(),
"protocol_version": "3",
"connection_timeout": "5",
"initial_connection_timeout": "5",
"simple_retry_policy_retries": "3",
}, logger)
if err != nil {
t.Fatalf("Failed to create new backend: %v", err)
}
physical.ExerciseBackend(t, b)
physical.ExerciseBackend_ListPrefix(t, b)
}
func TestCassandraBackendBuckets(t *testing.T) {
expectations := map[string][]string{
"": {"."},
"a": {"."},
"a/b": {".", "a"},
"a/b/c/d/e": {".", "a", "a/b", "a/b/c", "a/b/c/d"},
}
b := &CassandraBackend{}
for input, expected := range expectations {
actual := b.buckets(input)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("bad: %v expected: %v", actual, expected)
}
}
}

View File

@ -1,27 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package main
import (
"log"
"os"
"github.com/hashicorp/vault/plugins/database/cassandra"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
)
func main() {
err := Run()
if err != nil {
log.Println(err)
os.Exit(1)
}
}
// Run instantiates a Cassandra object, and runs the RPC server for the plugin
func Run() error {
dbplugin.ServeMultiplex(cassandra.New)
return nil
}

View File

@ -1,263 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/gocql/gocql"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/strutil"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
)
const (
defaultUserCreationCQL = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;`
defaultUserDeletionCQL = `DROP USER '{{username}}';`
defaultChangePasswordCQL = `ALTER USER '{{username}}' WITH PASSWORD '{{password}}';`
cassandraTypeName = "cassandra"
defaultUserNameTemplate = `{{ printf "v_%s_%s_%s_%s" (.DisplayName | truncate 15) (.RoleName | truncate 15) (random 20) (unix_time) | truncate 100 | replace "-" "_" | lowercase }}`
)
var _ dbplugin.Database = &Cassandra{}
// Cassandra is an implementation of Database interface
type Cassandra struct {
*cassandraConnectionProducer
usernameProducer template.StringTemplate
}
// New returns a new Cassandra instance
func New() (interface{}, error) {
db := new()
dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
return dbType, nil
}
func new() *Cassandra {
connProducer := &cassandraConnectionProducer{}
connProducer.Type = cassandraTypeName
return &Cassandra{
cassandraConnectionProducer: connProducer,
}
}
// Type returns the TypeName for this backend
func (c *Cassandra) Type() (string, error) {
return cassandraTypeName, nil
}
func (c *Cassandra) getConnection(ctx context.Context) (*gocql.Session, error) {
session, err := c.Connection(ctx)
if err != nil {
return nil, err
}
return session.(*gocql.Session), nil
}
func (c *Cassandra) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) {
usernameTemplate, err := strutil.GetString(req.Config, "username_template")
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve username_template: %w", err)
}
if usernameTemplate == "" {
usernameTemplate = defaultUserNameTemplate
}
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize username template: %w", err)
}
c.usernameProducer = up
_, err = c.usernameProducer.Generate(dbplugin.UsernameMetadata{})
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err)
}
err = c.cassandraConnectionProducer.Initialize(ctx, req)
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("failed to initialize: %w", err)
}
resp := dbplugin.InitializeResponse{
Config: req.Config,
}
return resp, nil
}
// NewUser generates the username/password on the underlying Cassandra secret backend as instructed by
// the statements provided.
func (c *Cassandra) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) {
c.Lock()
defer c.Unlock()
session, err := c.getConnection(ctx)
if err != nil {
return dbplugin.NewUserResponse{}, err
}
creationCQL := req.Statements.Commands
if len(creationCQL) == 0 {
creationCQL = []string{defaultUserCreationCQL}
}
rollbackCQL := req.RollbackStatements.Commands
if len(rollbackCQL) == 0 {
rollbackCQL = []string{defaultUserDeletionCQL}
}
username, err := c.usernameProducer.Generate(req.UsernameConfig)
if err != nil {
return dbplugin.NewUserResponse{}, err
}
for _, stmt := range creationCQL {
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
m := map[string]string{
"username": username,
"password": req.Password,
}
err = session.
Query(dbutil.QueryHelper(query, m)).
WithContext(ctx).
Exec()
if err != nil {
rollbackErr := rollbackUser(ctx, session, username, rollbackCQL)
if rollbackErr != nil {
err = multierror.Append(err, rollbackErr)
}
return dbplugin.NewUserResponse{}, err
}
}
}
resp := dbplugin.NewUserResponse{
Username: username,
}
return resp, nil
}
func rollbackUser(ctx context.Context, session *gocql.Session, username string, rollbackCQL []string) error {
for _, stmt := range rollbackCQL {
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
m := map[string]string{
"username": username,
}
err := session.
Query(dbutil.QueryHelper(query, m)).
WithContext(ctx).
Exec()
if err != nil {
return fmt.Errorf("failed to roll back user %s: %w", username, err)
}
}
}
return nil
}
func (c *Cassandra) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) {
if req.Password == nil && req.Expiration == nil {
return dbplugin.UpdateUserResponse{}, fmt.Errorf("no changes requested")
}
if req.Password != nil {
err := c.changeUserPassword(ctx, req.Username, req.Password)
return dbplugin.UpdateUserResponse{}, err
}
// Expiration is no-op
return dbplugin.UpdateUserResponse{}, nil
}
func (c *Cassandra) changeUserPassword(ctx context.Context, username string, changePass *dbplugin.ChangePassword) error {
session, err := c.getConnection(ctx)
if err != nil {
return err
}
rotateCQL := changePass.Statements.Commands
if len(rotateCQL) == 0 {
rotateCQL = []string{defaultChangePasswordCQL}
}
var result *multierror.Error
for _, stmt := range rotateCQL {
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
m := map[string]string{
"username": username,
"password": changePass.NewPassword,
}
err := session.
Query(dbutil.QueryHelper(query, m)).
WithContext(ctx).
Exec()
result = multierror.Append(result, err)
}
}
return result.ErrorOrNil()
}
// DeleteUser attempts to drop the specified user.
func (c *Cassandra) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) {
c.Lock()
defer c.Unlock()
session, err := c.getConnection(ctx)
if err != nil {
return dbplugin.DeleteUserResponse{}, err
}
revocationCQL := req.Statements.Commands
if len(revocationCQL) == 0 {
revocationCQL = []string{defaultUserDeletionCQL}
}
var result *multierror.Error
for _, stmt := range revocationCQL {
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
m := map[string]string{
"username": req.Username,
}
err := session.
Query(dbutil.QueryHelper(query, m)).
WithContext(ctx).
Exec()
result = multierror.Append(result, err)
}
}
return dbplugin.DeleteUserResponse{}, result.ErrorOrNil()
}

View File

@ -1,308 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"context"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/require"
backoff "github.com/cenkalti/backoff/v3"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/helper/testhelpers/cassandra"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
)
func getCassandra(t *testing.T, protocolVersion interface{}) (*Cassandra, func()) {
host, cleanup := cassandra.PrepareTestContainer(t,
cassandra.Version("3.11"),
cassandra.CopyFromTo(insecureFileMounts),
)
db := new()
initReq := dbplugin.InitializeRequest{
Config: map[string]interface{}{
"hosts": host.ConnectionURL(),
"port": host.Port,
"username": "cassandra",
"password": "cassandra",
"protocol_version": protocolVersion,
"connect_timeout": "20s",
},
VerifyConnection: true,
}
expectedConfig := map[string]interface{}{
"hosts": host.ConnectionURL(),
"port": host.Port,
"username": "cassandra",
"password": "cassandra",
"protocol_version": protocolVersion,
"connect_timeout": "20s",
}
initResp := dbtesting.AssertInitialize(t, db, initReq)
if !reflect.DeepEqual(initResp.Config, expectedConfig) {
t.Fatalf("Initialize response config actual: %#v\nExpected: %#v", initResp.Config, expectedConfig)
}
if !db.Initialized {
t.Fatal("Database should be initialized")
}
return db, cleanup
}
func TestInitialize(t *testing.T) {
t.Run("integer protocol version", func(t *testing.T) {
// getCassandra performs an Initialize call
db, cleanup := getCassandra(t, 4)
t.Cleanup(cleanup)
err := db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
})
t.Run("string protocol version", func(t *testing.T) {
// getCassandra performs an Initialize call
db, cleanup := getCassandra(t, "4")
t.Cleanup(cleanup)
err := db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
})
}
func TestCreateUser(t *testing.T) {
type testCase struct {
// Config will have the hosts & port added to it during the test
config map[string]interface{}
newUserReq dbplugin.NewUserRequest
expectErr bool
expectedUsernameRegex string
assertCreds func(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration)
}
tests := map[string]testCase{
"default username_template": {
config: map[string]interface{}{
"username": "cassandra",
"password": "cassandra",
"protocol_version": "4",
"connect_timeout": "20s",
},
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "token",
RoleName: "mylongrolenamewithmanycharacters",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: "bfn985wjAHIh6t",
Expiration: time.Now().Add(1 * time.Minute),
},
expectErr: false,
expectedUsernameRegex: `^v_token_mylongrolenamew_[a-z0-9]{20}_[0-9]{10}$`,
assertCreds: assertCreds,
},
"custom username_template": {
config: map[string]interface{}{
"username": "cassandra",
"password": "cassandra",
"protocol_version": "4",
"connect_timeout": "20s",
"username_template": `foo_{{random 20}}_{{.RoleName | replace "e" "3"}}_{{unix_time}}`,
},
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "token",
RoleName: "mylongrolenamewithmanycharacters",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: "bfn985wjAHIh6t",
Expiration: time.Now().Add(1 * time.Minute),
},
expectErr: false,
expectedUsernameRegex: `^foo_[a-zA-Z0-9]{20}_mylongrol3nam3withmanycharact3rs_[0-9]{10}$`,
assertCreds: assertCreds,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
host, cleanup := cassandra.PrepareTestContainer(t,
cassandra.Version("3.11"),
cassandra.CopyFromTo(insecureFileMounts),
)
defer cleanup()
db := new()
config := test.config
config["hosts"] = host.ConnectionURL()
config["port"] = host.Port
initReq := dbplugin.InitializeRequest{
Config: config,
VerifyConnection: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
dbtesting.AssertInitialize(t, db, initReq)
require.True(t, db.Initialized, "Database is not initialized")
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
newUserResp, err := db.NewUser(ctx, test.newUserReq)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
require.Regexp(t, test.expectedUsernameRegex, newUserResp.Username)
test.assertCreds(t, db.Hosts, db.Port, newUserResp.Username, test.newUserReq.Password, nil, 5*time.Second)
})
}
}
func TestUpdateUserPassword(t *testing.T) {
db, cleanup := getCassandra(t, 4)
defer cleanup()
password := "myreallysecurepassword"
createReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: password,
Expiration: time.Now().Add(1 * time.Minute),
}
createResp := dbtesting.AssertNewUser(t, db, createReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
newPassword := "somenewpassword"
updateReq := dbplugin.UpdateUserRequest{
Username: createResp.Username,
Password: &dbplugin.ChangePassword{
NewPassword: newPassword,
Statements: dbplugin.Statements{},
},
Expiration: nil,
}
dbtesting.AssertUpdateUser(t, db, updateReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, newPassword, nil, 5*time.Second)
}
func TestDeleteUser(t *testing.T) {
db, cleanup := getCassandra(t, 4)
defer cleanup()
password := "myreallysecurepassword"
createReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: password,
Expiration: time.Now().Add(1 * time.Minute),
}
createResp := dbtesting.AssertNewUser(t, db, createReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
deleteReq := dbplugin.DeleteUserRequest{
Username: createResp.Username,
}
dbtesting.AssertDeleteUser(t, db, deleteReq)
assertNoCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
}
func assertCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) {
t.Helper()
op := func() error {
return connect(t, address, port, username, password, sslOpts)
}
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = timeout
bo.InitialInterval = 500 * time.Millisecond
bo.MaxInterval = bo.InitialInterval
bo.RandomizationFactor = 0.0
err := backoff.Retry(op, bo)
if err != nil {
t.Fatalf("failed to connect after %s: %s", timeout, err)
}
}
func connect(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions) error {
t.Helper()
clusterConfig := gocql.NewCluster(address)
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
Username: username,
Password: password,
}
clusterConfig.ProtoVersion = 4
clusterConfig.Port = port
clusterConfig.SslOpts = sslOpts
session, err := clusterConfig.CreateSession()
if err != nil {
return err
}
defer session.Close()
return nil
}
func assertNoCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) {
t.Helper()
op := func() error {
// "Invert" the error so the backoff logic sees a failure to connect as a success
err := connect(t, address, port, username, password, sslOpts)
if err != nil {
return nil
}
return nil
}
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = timeout
bo.InitialInterval = 500 * time.Millisecond
bo.MaxInterval = bo.InitialInterval
bo.RandomizationFactor = 0.0
err := backoff.Retry(op, bo)
if err != nil {
t.Fatalf("successfully connected after %s when it shouldn't", timeout)
}
}
const createUserStatements = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;
GRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';`

View File

@ -1,244 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"context"
"crypto/tls"
"fmt"
"strings"
"sync"
"time"
"github.com/gocql/gocql"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/tlsutil"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/mitchellh/mapstructure"
)
// cassandraConnectionProducer implements ConnectionProducer and provides an
// interface for cassandra databases to make connections.
type cassandraConnectionProducer struct {
Hosts string `json:"hosts" structs:"hosts" mapstructure:"hosts"`
Port int `json:"port" structs:"port" mapstructure:"port"`
Username string `json:"username" structs:"username" mapstructure:"username"`
Password string `json:"password" structs:"password" mapstructure:"password"`
TLS bool `json:"tls" structs:"tls" mapstructure:"tls"`
InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls" mapstructure:"insecure_tls"`
TLSServerName string `json:"tls_server_name" structs:"tls_server_name" mapstructure:"tls_server_name"`
ProtocolVersion int `json:"protocol_version" structs:"protocol_version" mapstructure:"protocol_version"`
ConnectTimeoutRaw interface{} `json:"connect_timeout" structs:"connect_timeout" mapstructure:"connect_timeout"`
SocketKeepAliveRaw interface{} `json:"socket_keep_alive" structs:"socket_keep_alive" mapstructure:"socket_keep_alive"`
TLSMinVersion string `json:"tls_min_version" structs:"tls_min_version" mapstructure:"tls_min_version"`
Consistency string `json:"consistency" structs:"consistency" mapstructure:"consistency"`
LocalDatacenter string `json:"local_datacenter" structs:"local_datacenter" mapstructure:"local_datacenter"`
PemBundle string `json:"pem_bundle" structs:"pem_bundle" mapstructure:"pem_bundle"`
PemJSON string `json:"pem_json" structs:"pem_json" mapstructure:"pem_json"`
SkipVerification bool `json:"skip_verification" structs:"skip_verification" mapstructure:"skip_verification"`
connectTimeout time.Duration
socketKeepAlive time.Duration
sslOpts *gocql.SslOptions
rawConfig map[string]interface{}
Initialized bool
Type string
session *gocql.Session
sync.Mutex
}
func (c *cassandraConnectionProducer) Initialize(ctx context.Context, req dbplugin.InitializeRequest) error {
c.Lock()
defer c.Unlock()
c.rawConfig = req.Config
err := mapstructure.WeakDecode(req.Config, c)
if err != nil {
return err
}
if c.ConnectTimeoutRaw == nil {
c.ConnectTimeoutRaw = "5s"
}
c.connectTimeout, err = parseutil.ParseDurationSecond(c.ConnectTimeoutRaw)
if err != nil {
return fmt.Errorf("invalid connect_timeout: %w", err)
}
if c.SocketKeepAliveRaw == nil {
c.SocketKeepAliveRaw = "0s"
}
c.socketKeepAlive, err = parseutil.ParseDurationSecond(c.SocketKeepAliveRaw)
if err != nil {
return fmt.Errorf("invalid socket_keep_alive: %w", err)
}
switch {
case len(c.Hosts) == 0:
return fmt.Errorf("hosts cannot be empty")
case len(c.Username) == 0:
return fmt.Errorf("username cannot be empty")
case len(c.Password) == 0:
return fmt.Errorf("password cannot be empty")
case len(c.PemJSON) > 0 && len(c.PemBundle) > 0:
return fmt.Errorf("cannot specify both pem_json and pem_bundle")
}
var tlsMinVersion uint16 = tls.VersionTLS12
if c.TLSMinVersion != "" {
ver, exists := tlsutil.TLSLookup[c.TLSMinVersion]
if !exists {
return fmt.Errorf("unrecognized TLS version [%s]", c.TLSMinVersion)
}
tlsMinVersion = ver
}
switch {
case len(c.PemJSON) != 0:
cfg, err := jsonBundleToTLSConfig(c.PemJSON, tlsMinVersion, c.TLSServerName, c.InsecureTLS)
if err != nil {
return fmt.Errorf("failed to parse pem_json: %w", err)
}
c.sslOpts = &gocql.SslOptions{
Config: cfg,
EnableHostVerification: !cfg.InsecureSkipVerify,
}
c.TLS = true
case len(c.PemBundle) != 0:
cfg, err := pemBundleToTLSConfig(c.PemBundle, tlsMinVersion, c.TLSServerName, c.InsecureTLS)
if err != nil {
return fmt.Errorf("failed to parse pem_bundle: %w", err)
}
c.sslOpts = &gocql.SslOptions{
Config: cfg,
EnableHostVerification: !cfg.InsecureSkipVerify,
}
c.TLS = true
case c.InsecureTLS:
c.sslOpts = &gocql.SslOptions{
EnableHostVerification: !c.InsecureTLS,
}
}
// Set initialized to true at this point since all fields are set,
// and the connection can be established at a later time.
c.Initialized = true
if req.VerifyConnection {
if _, err := c.Connection(ctx); err != nil {
return fmt.Errorf("error verifying connection: %w", err)
}
}
return nil
}
func (c *cassandraConnectionProducer) Connection(ctx context.Context) (interface{}, error) {
if !c.Initialized {
return nil, connutil.ErrNotInitialized
}
// If we already have a DB, return it
if c.session != nil && !c.session.Closed() {
return c.session, nil
}
session, err := c.createSession(ctx)
if err != nil {
return nil, err
}
// Store the session in backend for reuse
c.session = session
return session, nil
}
func (c *cassandraConnectionProducer) Close() error {
c.Lock()
defer c.Unlock()
if c.session != nil {
c.session.Close()
}
c.session = nil
return nil
}
func (c *cassandraConnectionProducer) createSession(ctx context.Context) (*gocql.Session, error) {
hosts := strings.Split(c.Hosts, ",")
clusterConfig := gocql.NewCluster(hosts...)
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
Username: c.Username,
Password: c.Password,
}
if c.Port != 0 {
clusterConfig.Port = c.Port
}
clusterConfig.ProtoVersion = c.ProtocolVersion
if clusterConfig.ProtoVersion == 0 {
clusterConfig.ProtoVersion = 2
}
clusterConfig.Timeout = c.connectTimeout
clusterConfig.ConnectTimeout = c.connectTimeout
clusterConfig.SocketKeepalive = c.socketKeepAlive
clusterConfig.SslOpts = c.sslOpts
if c.LocalDatacenter != "" {
clusterConfig.PoolConfig.HostSelectionPolicy = gocql.DCAwareRoundRobinPolicy(c.LocalDatacenter)
}
session, err := clusterConfig.CreateSession()
if err != nil {
return nil, fmt.Errorf("error creating session: %w", err)
}
if c.Consistency != "" {
consistencyValue, err := gocql.ParseConsistencyWrapper(c.Consistency)
if err != nil {
session.Close()
return nil, err
}
session.SetConsistency(consistencyValue)
}
if !c.SkipVerification {
err = session.Query(`LIST ALL`).WithContext(ctx).Exec()
if err != nil && len(c.Username) != 0 && strings.Contains(err.Error(), "not authorized") {
rowNum := session.Query(dbutil.QueryHelper(`LIST CREATE ON ALL ROLES OF '{{username}}';`, map[string]string{
"username": c.Username,
})).Iter().NumRows()
if rowNum < 1 {
session.Close()
return nil, fmt.Errorf("error validating connection info: No role create permissions found, previous error: %w", err)
}
} else if err != nil {
session.Close()
return nil, fmt.Errorf("error validating connection info: %w", err)
}
}
return session, nil
}
func (c *cassandraConnectionProducer) secretValues() map[string]string {
return map[string]string{
c.Password: "[password]",
c.PemBundle: "[pem_bundle]",
c.PemJSON: "[pem_json]",
}
}

View File

@ -1,233 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEFjCCAv6gAwIBAgIUHNknw0iUWaMC5UCpiribG8DQhZYwDQYJKoZIhvcNAQEL
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjE0MjAyNDAwWhcNMjYwNjEz
MjAyNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE
CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw
ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANc0MEZOJ7xm4JrCceerX0kWcdPIczXFIIZTJYdTB7YPHTiL
PFSZ9ugu8W6R7wOMLUazcD7Ugw0hjt+JkiRIY1AOvuZRX7DR3Q0sGy9qFb1y2kOk
lTSAFOV96FxxAg9Fn23mcvjV1TDO1dlxvOuAo0NMjk82TzHk7LVuYOKuJ/Sc9i8a
Ba4vndbiwkSGpytymCu0X4T4ZEARLUZ4feGhr5RbYRehq2Nb8kw/KNLZZyzlzJbr
8OkVizW796bkVJwRfCFubZPl8EvRslxZ2+sMFSozoofoFlB1FsGAvlnEfkxqTJJo
WafmsYnOVnbNfwOogDP0+bp8WAZrAxJqTAWm/LMCAwEAAaNCMEAwDgYDVR0PAQH/
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHyfBUnvAULGlcFSljTI
DegUVLB5MA0GCSqGSIb3DQEBCwUAA4IBAQBOdVqZpMCKq+X2TBi3nJmz6kjePVBh
ocHUG02nRkL533x+PUxRpDG3AMzWF3niPxtMuVIZDfpi27zlm2QCh9b3sQi83w+9
UX1/j3dUoUyiVi/U0iZeZmuDY3ne59DNFdOgGY9p3FvJ+b9WfPg8+v2w26rGoSMz
21XKNZcRFcjOJ5LJ3i9+liaCkpXLfErA+AtqNeraHOorJ5UO4mA7OlFowV8adOQq
SinFIoXCExBTxqMv0lVzEhGN6Wd261CmKY5e4QLqASCO+s7zwGhHyzwjdA0pCNtI
PmHIk13m0p56G8hpz+M/5hBQFb0MIIR3Je6QVzfRty2ipUO91E9Ydm7C
-----END CERTIFICATE-----

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5
MjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE
CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw
ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka
qZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3
T7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT
LYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI
6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi
KHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl
ADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS
rbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1
aKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF
+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV
CNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T
aNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD
-----END CERTIFICATE-----

View File

@ -1,3 +0,0 @@
{
"ca_chain": ["-----BEGIN CERTIFICATE-----\nMIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL\nBQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl\nc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz\ndCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5\nMjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU\nBgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE\nCxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw\nZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka\nqZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3\nT7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT\nLYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI\n6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi\nKHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl\nADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS\nrbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1\naKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF\n+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV\nCNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T\naNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD\n-----END CERTIFICATE-----\n"]
}

View File

@ -1,3 +0,0 @@
[ssl]
validate = false
version = SSLv23

View File

@ -1,120 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cassandra
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil"
)
func jsonBundleToTLSConfig(rawJSON string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
var certBundle certutil.CertBundle
err := json.Unmarshal([]byte(rawJSON), &certBundle)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
if certBundle.IssuingCA != "" && len(certBundle.CAChain) > 0 {
return nil, fmt.Errorf("issuing_ca and ca_chain cannot both be specified")
}
if certBundle.IssuingCA != "" {
certBundle.CAChain = []string{certBundle.IssuingCA}
certBundle.IssuingCA = ""
}
return toClientTLSConfig(certBundle.Certificate, certBundle.PrivateKey, certBundle.CAChain, tlsMinVersion, serverName, insecureSkipVerify)
}
func pemBundleToTLSConfig(pemBundle string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
if len(pemBundle) == 0 {
return nil, errutil.UserError{Err: "empty pem bundle"}
}
pemBytes := []byte(pemBundle)
var pemBlock *pem.Block
certificate := ""
privateKey := ""
caChain := []string{}
for len(pemBytes) > 0 {
pemBlock, pemBytes = pem.Decode(pemBytes)
if pemBlock == nil {
return nil, errutil.UserError{Err: "no data found in PEM block"}
}
blockBytes := pem.EncodeToMemory(pemBlock)
switch pemBlock.Type {
case "CERTIFICATE":
// Parse the cert so we know if it's a CA or not
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
if cert.IsCA {
caChain = append(caChain, string(blockBytes))
continue
}
// Only one leaf certificate supported
if certificate != "" {
return nil, errutil.UserError{Err: "multiple leaf certificates not supported"}
}
certificate = string(blockBytes)
case "RSA PRIVATE KEY", "EC PRIVATE KEY", "PRIVATE KEY":
if privateKey != "" {
return nil, errutil.UserError{Err: "multiple private keys not supported"}
}
privateKey = string(blockBytes)
default:
return nil, fmt.Errorf("unsupported PEM block type [%s]", pemBlock.Type)
}
}
return toClientTLSConfig(certificate, privateKey, caChain, tlsMinVersion, serverName, insecureSkipVerify)
}
func toClientTLSConfig(certificatePEM string, privateKeyPEM string, caChainPEMs []string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
if certificatePEM != "" && privateKeyPEM == "" {
return nil, fmt.Errorf("found certificate for client-side TLS authentication but no private key")
} else if certificatePEM == "" && privateKeyPEM != "" {
return nil, fmt.Errorf("found private key for client-side TLS authentication but no certificate")
}
var certificates []tls.Certificate
if certificatePEM != "" {
certificate, err := tls.X509KeyPair([]byte(certificatePEM), []byte(privateKeyPEM))
if err != nil {
return nil, fmt.Errorf("failed to parse certificate and private key pair: %w", err)
}
certificates = append(certificates, certificate)
}
var rootCAs *x509.CertPool
if len(caChainPEMs) > 0 {
rootCAs = x509.NewCertPool()
for _, caBlock := range caChainPEMs {
ok := rootCAs.AppendCertsFromPEM([]byte(caBlock))
if !ok {
return nil, fmt.Errorf("failed to add CA certificate to certificate pool: it may be malformed or empty")
}
}
}
config := &tls.Config{
Certificates: certificates,
RootCAs: rootCAs,
ServerName: serverName,
InsecureSkipVerify: insecureSkipVerify,
MinVersion: tlsMinVersion,
}
return config, nil
}