Compare commits
2 Commits
afabbdd1ce
...
674a55933f
Author | SHA1 | Date | |
---|---|---|---|
674a55933f | |||
46f1aaa3bf |
5
Makefile
5
Makefile
@ -307,6 +307,9 @@ 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
|
||||
|
||||
@ -363,7 +366,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 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 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
|
||||
|
||||
.NOTPARALLEL: ember-dist ember-dist-dev
|
||||
|
||||
|
@ -41,7 +41,6 @@ import (
|
||||
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||||
logicalDb "github.com/hashicorp/vault/builtin/logical/database"
|
||||
|
||||
physAerospike "github.com/hashicorp/vault/physical/aerospike"
|
||||
physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb"
|
||||
physConsul "github.com/hashicorp/vault/physical/consul"
|
||||
physFoundationDB "github.com/hashicorp/vault/physical/foundationdb"
|
||||
@ -169,7 +168,6 @@ var (
|
||||
}
|
||||
|
||||
physicalBackends = map[string]physical.Factory{
|
||||
"aerospike": physAerospike.NewAerospikeBackend,
|
||||
"cockroachdb": physCockroachDB.NewCockroachDBBackend,
|
||||
"consul": physConsul.NewConsulBackend,
|
||||
"file_transactional": physFile.NewTransactionalFileBackend,
|
||||
|
6
go.mod
6
go.mod
@ -26,12 +26,12 @@ replace github.com/hashicorp/vault/sdk => ./sdk
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec
|
||||
github.com/aerospike/aerospike-client-go/v5 v5.6.0
|
||||
github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2
|
||||
github.com/armon/go-metrics v0.4.1
|
||||
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
|
||||
@ -46,6 +46,7 @@ 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
|
||||
@ -213,7 +214,6 @@ 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
|
||||
@ -270,6 +270,7 @@ 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
|
||||
@ -353,7 +354,6 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vmware/govmomi v0.18.0 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.6 // indirect
|
||||
|
13
go.sum
13
go.sum
@ -936,8 +936,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14=
|
||||
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
|
||||
github.com/aerospike/aerospike-client-go/v5 v5.6.0 h1:tRxcUq0HY8fFPQEzF3EgrknF+w1xFO0YDfUb9Nm8yRI=
|
||||
github.com/aerospike/aerospike-client-go/v5 v5.6.0/go.mod h1:rJ/KpmClE7kiBPfvAPrGw9WuNOiz8v2uKbQaUyYPXtI=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
@ -1006,12 +1004,15 @@ 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=
|
||||
@ -1633,6 +1634,8 @@ 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=
|
||||
@ -1846,6 +1849,8 @@ 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=
|
||||
@ -2868,9 +2873,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
@ -3309,7 +3311,6 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -31,6 +31,7 @@ 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"
|
||||
@ -109,6 +110,7 @@ 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},
|
||||
},
|
||||
@ -117,6 +119,10 @@ 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},
|
||||
|
178
helper/testhelpers/cassandra/cassandrahelper.go
Normal file
178
helper/testhelpers/cassandra/cassandrahelper.go
Normal file
@ -0,0 +1,178 @@
|
||||
// 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
|
||||
}
|
@ -155,6 +155,7 @@ 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",
|
||||
}
|
||||
|
@ -1,254 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package aerospike
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aero "github.com/aerospike/aerospike-client-go/v5"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/hashicorp/vault/sdk/physical"
|
||||
)
|
||||
|
||||
const (
|
||||
keyBin = "keyBin"
|
||||
valueBin = "valueBin"
|
||||
|
||||
defaultNamespace = "test"
|
||||
|
||||
defaultHostname = "127.0.0.1"
|
||||
defaultPort = 3000
|
||||
|
||||
keyNotFoundError = "Key not found"
|
||||
)
|
||||
|
||||
// AerospikeBackend is a physical backend that stores data in Aerospike.
|
||||
type AerospikeBackend struct {
|
||||
client *aero.Client
|
||||
namespace string
|
||||
set string
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Verify AerospikeBackend satisfies the correct interface.
|
||||
var _ physical.Backend = (*AerospikeBackend)(nil)
|
||||
|
||||
// NewAerospikeBackend constructs an AerospikeBackend backend.
|
||||
func NewAerospikeBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
|
||||
namespace, ok := conf["namespace"]
|
||||
if !ok {
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
set := conf["set"]
|
||||
|
||||
policy, err := buildClientPolicy(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := buildAerospikeClient(conf, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AerospikeBackend{
|
||||
client: client,
|
||||
namespace: namespace,
|
||||
set: set,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAerospikeClient(conf map[string]string, policy *aero.ClientPolicy) (*aero.Client, error) {
|
||||
hostListString, ok := conf["hostlist"]
|
||||
if !ok || hostListString == "" {
|
||||
hostname, ok := conf["hostname"]
|
||||
if !ok || hostname == "" {
|
||||
hostname = defaultHostname
|
||||
}
|
||||
|
||||
portString, ok := conf["port"]
|
||||
if !ok || portString == "" {
|
||||
portString = strconv.Itoa(defaultPort)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aero.NewClientWithPolicy(policy, hostname, port)
|
||||
}
|
||||
|
||||
hostList, err := parseHostList(hostListString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aero.NewClientWithPolicyAndHost(policy, hostList...)
|
||||
}
|
||||
|
||||
func buildClientPolicy(conf map[string]string) (*aero.ClientPolicy, error) {
|
||||
policy := aero.NewClientPolicy()
|
||||
|
||||
policy.User = conf["username"]
|
||||
policy.Password = conf["password"]
|
||||
|
||||
authMode := aero.AuthModeInternal
|
||||
if mode, ok := conf["auth_mode"]; ok {
|
||||
switch strings.ToUpper(mode) {
|
||||
case "EXTERNAL":
|
||||
authMode = aero.AuthModeExternal
|
||||
case "INTERNAL":
|
||||
authMode = aero.AuthModeInternal
|
||||
default:
|
||||
return nil, fmt.Errorf("'auth_mode' must be one of {INTERNAL, EXTERNAL}")
|
||||
}
|
||||
}
|
||||
policy.AuthMode = authMode
|
||||
policy.ClusterName = conf["cluster_name"]
|
||||
|
||||
if timeoutString, ok := conf["timeout"]; ok {
|
||||
timeout, err := strconv.Atoi(timeoutString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.Timeout = time.Duration(timeout) * time.Millisecond
|
||||
}
|
||||
|
||||
if idleTimeoutString, ok := conf["idle_timeout"]; ok {
|
||||
idleTimeout, err := strconv.Atoi(idleTimeoutString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.IdleTimeout = time.Duration(idleTimeout) * time.Millisecond
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (a *AerospikeBackend) key(userKey string) (*aero.Key, error) {
|
||||
return aero.NewKey(a.namespace, a.set, hash(userKey))
|
||||
}
|
||||
|
||||
// Put is used to insert or update an entry.
|
||||
func (a *AerospikeBackend) Put(_ context.Context, entry *physical.Entry) error {
|
||||
aeroKey, err := a.key(entry.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// replace the Aerospike record if exists
|
||||
writePolicy := aero.NewWritePolicy(0, 0)
|
||||
writePolicy.RecordExistsAction = aero.REPLACE
|
||||
|
||||
binMap := make(aero.BinMap, 2)
|
||||
binMap[keyBin] = entry.Key
|
||||
binMap[valueBin] = entry.Value
|
||||
|
||||
return a.client.Put(writePolicy, aeroKey, binMap)
|
||||
}
|
||||
|
||||
// Get is used to fetch an entry.
|
||||
func (a *AerospikeBackend) Get(_ context.Context, key string) (*physical.Entry, error) {
|
||||
aeroKey, err := a.key(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err := a.client.Get(nil, aeroKey)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), keyNotFoundError) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, ok := record.Bins[valueBin]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Value bin was not found in the record")
|
||||
}
|
||||
|
||||
return &physical.Entry{
|
||||
Key: key,
|
||||
Value: value.([]byte),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete is used to permanently delete an entry.
|
||||
func (a *AerospikeBackend) Delete(_ context.Context, key string) error {
|
||||
aeroKey, err := a.key(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = a.client.Delete(nil, aeroKey)
|
||||
return err
|
||||
}
|
||||
|
||||
// List is used to list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
func (a *AerospikeBackend) List(_ context.Context, prefix string) ([]string, error) {
|
||||
recordSet, err := a.client.ScanAll(nil, a.namespace, a.set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keyList []string
|
||||
for res := range recordSet.Results() {
|
||||
if res.Err != nil {
|
||||
return nil, res.Err
|
||||
}
|
||||
recordKey := res.Record.Bins[keyBin].(string)
|
||||
if strings.HasPrefix(recordKey, prefix) {
|
||||
trimPrefix := strings.TrimPrefix(recordKey, prefix)
|
||||
keys := strings.Split(trimPrefix, "/")
|
||||
if len(keys) == 1 {
|
||||
keyList = append(keyList, keys[0])
|
||||
} else {
|
||||
withSlash := keys[0] + "/"
|
||||
if !strutil.StrListContains(keyList, withSlash) {
|
||||
keyList = append(keyList, withSlash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyList, nil
|
||||
}
|
||||
|
||||
func parseHostList(list string) ([]*aero.Host, error) {
|
||||
hosts := strings.Split(list, ",")
|
||||
var hostList []*aero.Host
|
||||
for _, host := range hosts {
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
split := strings.Split(host, ":")
|
||||
switch len(split) {
|
||||
case 1:
|
||||
hostList = append(hostList, aero.NewHost(split[0], defaultPort))
|
||||
case 2:
|
||||
port, err := strconv.Atoi(split[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostList = append(hostList, aero.NewHost(split[0], port))
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid 'hostlist' configuration")
|
||||
}
|
||||
}
|
||||
return hostList, nil
|
||||
}
|
||||
|
||||
func hash(s string) string {
|
||||
hash := sha256.Sum256([]byte(s))
|
||||
return fmt.Sprintf("%x", hash[:])
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package aerospike
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/bits"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
aero "github.com/aerospike/aerospike-client-go/v5"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/sdk/helper/docker"
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
"github.com/hashicorp/vault/sdk/physical"
|
||||
)
|
||||
|
||||
func TestAerospikeBackend(t *testing.T) {
|
||||
if bits.UintSize == 32 {
|
||||
t.Skip("Aerospike storage is only supported on 64-bit architectures")
|
||||
}
|
||||
cleanup, config := prepareAerospikeContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
|
||||
b, err := NewAerospikeBackend(map[string]string{
|
||||
"hostname": config.hostname,
|
||||
"port": config.port,
|
||||
"namespace": config.namespace,
|
||||
"set": config.set,
|
||||
}, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
physical.ExerciseBackend(t, b)
|
||||
physical.ExerciseBackend_ListPrefix(t, b)
|
||||
}
|
||||
|
||||
type aerospikeConfig struct {
|
||||
hostname string
|
||||
port string
|
||||
namespace string
|
||||
set string
|
||||
}
|
||||
|
||||
func prepareAerospikeContainer(t *testing.T) (func(), *aerospikeConfig) {
|
||||
runner, err := docker.NewServiceRunner(docker.RunOptions{
|
||||
ImageRepo: "docker.mirror.hashicorp.services/aerospike/aerospike-server",
|
||||
ContainerName: "aerospikedb",
|
||||
ImageTag: "5.6.0.5",
|
||||
Ports: []string{"3000/tcp", "3001/tcp", "3002/tcp", "3003/tcp"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start local Aerospike: %s", err)
|
||||
}
|
||||
|
||||
svc, err := runner.StartService(context.Background(),
|
||||
func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
|
||||
cfg := docker.NewServiceHostPort(host, port)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
client, err := aero.NewClient(host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node, err := client.Cluster().GetRandomNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = node.RequestInfo(aero.NewInfoPolicy(), "namespaces")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start local Aerospike: %s", err)
|
||||
}
|
||||
|
||||
return svc.Cleanup, &aerospikeConfig{
|
||||
hostname: svc.Config.URL().Hostname(),
|
||||
port: svc.Config.URL().Port(),
|
||||
namespace: "test",
|
||||
set: "vault",
|
||||
}
|
||||
}
|
27
plugins/database/cassandra/cassandra-database-plugin/main.go
Normal file
27
plugins/database/cassandra/cassandra-database-plugin/main.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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
|
||||
}
|
263
plugins/database/cassandra/cassandra.go
Normal file
263
plugins/database/cassandra/cassandra.go
Normal file
@ -0,0 +1,263 @@
|
||||
// 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()
|
||||
}
|
308
plugins/database/cassandra/cassandra_test.go
Normal file
308
plugins/database/cassandra/cassandra_test.go
Normal file
@ -0,0 +1,308 @@
|
||||
// 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}}';`
|
244
plugins/database/cassandra/connection_producer.go
Normal file
244
plugins/database/cassandra/connection_producer.go
Normal file
@ -0,0 +1,244 @@
|
||||
// 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]",
|
||||
}
|
||||
}
|
233
plugins/database/cassandra/connection_producer_test.go
Normal file
233
plugins/database/cassandra/connection_producer_test.go
Normal file
@ -0,0 +1,233 @@
|
||||
// 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)
|
||||
}
|
1149
plugins/database/cassandra/test-fixtures/no_tls/cassandra.yaml
Normal file
1149
plugins/database/cassandra/test-fixtures/no_tls/cassandra.yaml
Normal file
File diff suppressed because it is too large
Load Diff
24
plugins/database/cassandra/test-fixtures/with_tls/bad_ca.pem
Normal file
24
plugins/database/cassandra/test-fixtures/with_tls/bad_ca.pem
Normal file
@ -0,0 +1,24 @@
|
||||
-----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-----
|
24
plugins/database/cassandra/test-fixtures/with_tls/ca.pem
Normal file
24
plugins/database/cassandra/test-fixtures/with_tls/ca.pem
Normal file
@ -0,0 +1,24 @@
|
||||
-----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-----
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
[ssl]
|
||||
validate = false
|
||||
version = SSLv23
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
120
plugins/database/cassandra/tls.go
Normal file
120
plugins/database/cassandra/tls.go
Normal file
@ -0,0 +1,120 @@
|
||||
// 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user