Files
teleport/lib/srv/db/ca_test.go
T

1004 lines
29 KiB
Go

/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package db
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cloud/mocks"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/mongodb"
"github.com/gravitational/teleport/lib/srv/db/mysql"
"github.com/gravitational/teleport/lib/srv/db/postgres"
"github.com/gravitational/teleport/lib/tlsca"
)
// TestInitCACert verifies automatic download of root certs for cloud databases.
func TestInitCACert(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
selfHosted, err := types.NewDatabaseV3(types.Metadata{
Name: "self-hosted",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
})
require.NoError(t, err)
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
rdsWithCert, err := types.NewDatabaseV3(types.Metadata{
Name: "rds-with-cert",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
CACert: "rds-test-cert",
})
require.NoError(t, err)
redshift, err := types.NewDatabaseV3(types.Metadata{
Name: "redshift",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1", Redshift: types.Redshift{ClusterID: "cluster-id"}},
})
require.NoError(t, err)
redshiftServerless, err := types.NewDatabaseV3(types.Metadata{
Name: "redshift",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{
Region: "us-east-1",
AccountID: "123456789012",
RedshiftServerless: types.RedshiftServerless{
WorkgroupName: "workgroup",
},
},
})
require.NoError(t, err)
memoryDB, err := types.NewDatabaseV3(types.Metadata{
Name: "memorydb",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1", MemoryDB: types.MemoryDB{ClusterName: "cluster"}},
})
require.NoError(t, err)
cloudSQL, err := types.NewDatabaseV3(types.Metadata{
Name: "cloud-sql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
GCP: types.GCPCloudSQL{ProjectID: "project-id", InstanceID: "instance-id"},
})
require.NoError(t, err)
azureMySQL, err := types.NewDatabaseV3(types.Metadata{
Name: "azure-mysql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "localhost:3306",
Azure: types.Azure{Name: "azure-mysql"},
})
require.NoError(t, err)
allDatabases := []types.Database{
selfHosted, rds, rdsWithCert, redshift, redshiftServerless, cloudSQL, azureMySQL, memoryDB,
}
tests := []struct {
desc string
database string
cert string
}{
{
desc: "shouldn't download self-hosted CA",
database: selfHosted.GetName(),
cert: selfHosted.GetCA(),
},
{
desc: "should download RDS CA when it's not set",
database: rds.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "shouldn't download RDS CA when it's set",
database: rdsWithCert.GetName(),
cert: rdsWithCert.GetCA(),
},
{
desc: "should download Redshift CA when it's not set",
database: redshift.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "should download Redshift Serverless CA when it's not set",
database: redshiftServerless.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "should download MemoryDB CA when it's not set",
database: memoryDB.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "should download Cloud SQL CA when it's not set",
database: cloudSQL.GetName(),
cert: fixtures.TLSCACertPEM,
},
{
desc: "should download Azure CA when it's not set",
database: azureMySQL.GetName(),
cert: fixtures.TLSCACertPEM + "\n" + fixtures.TLSCACertPEM, // Two CA files.
},
}
databaseServer := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: allDatabases,
})
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
var database types.Database
for _, db := range databaseServer.getProxiedDatabases() {
if db.GetName() == test.database {
database = db
}
}
require.NotNil(t, database)
require.Equal(t, test.cert, database.GetCA())
})
}
}
// TestInitCACertCaching verifies root certificate is not re-downloaded if
// it was already downloaded before.
func TestInitCACertCaching(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
databaseServer := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: []types.Database{rds},
NoStart: true,
})
// Initialize RDS cert for the first time.
require.NoError(t, databaseServer.initCACert(ctx, rds))
require.Equal(t, int64(1), databaseServer.cfg.CADownloader.(*fakeDownloader).count)
// Reset it and initialize again, it should already be downloaded.
rds.SetStatusCA("")
require.NoError(t, databaseServer.initCACert(ctx, rds))
require.Equal(t, int64(2), databaseServer.cfg.CADownloader.(*fakeDownloader).count)
}
// TestUpdateCACerts given a cloud-hosted database, update the cached CA files
// when the CA version changes.
func TestUpdateCACerts(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
initialCA := generateDatabaseCA(t)
caDownloader := &fakeDownloader{
cert: initialCA,
version: []byte("initial"),
}
databaseServer := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: []types.Database{rds},
NoStart: true,
CADownloader: caDownloader,
})
// Initialize the CA certs as normal.
require.NoError(t, databaseServer.initCACert(ctx, rds))
require.Equal(t, string(initialCA), rds.GetStatusCA())
// Change CA version and content in the downloader.
updatedCA := generateDatabaseCA(t)
caDownloader.cert = updatedCA
caDownloader.version = []byte("second-version")
// Trigger the update.
newCAContents, err := databaseServer.getCACerts(ctx, rds)
require.NoError(t, err)
require.Equal(t, updatedCA, newCAContents)
// Fetch CA certificate from the cached files.
cached, err := databaseServer.getCACerts(ctx, rds)
require.NoError(t, err)
require.Equal(t, updatedCA, cached)
}
// TestCARenewer given a list of started databases, renew their CA every 24 hour
// ensuring only the CA that have changed contents are updated.
func TestCARenewer(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
initialCA := generateDatabaseCA(t)
caDownloader := &fakeDownloader{
cert: initialCA,
version: []byte("initial"),
}
databaseServer := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: []types.Database{rds},
NoStart: true,
CADownloader: caDownloader,
})
// Initialize the CA certs as normal.
require.NoError(t, databaseServer.initCACert(ctx, rds))
require.Equal(t, string(initialCA), rds.GetStatusCA())
// Start the database CA renewer.
renewerCtx, cancel := context.WithCancel(ctx)
defer cancel()
caRenewerChan := make(chan struct{})
go func() {
databaseServer.startCARenewer(renewerCtx)
caRenewerChan <- struct{}{}
}()
// Change CA version and content in the downloader.
updatedCA := generateDatabaseCA(t)
caDownloader.cert = updatedCA
caDownloader.version = []byte("second-version")
// Trigger the CA renews by advancing in time.
testCtx.clock.Advance(caRenewInterval)
// Check if the database status CA is updated with new contents.
require.Eventually(t, func() bool {
return atomic.LoadInt64(&caDownloader.count) == 2
}, 5*time.Second, time.Second, "failed to wait the CA download")
// Advance another time to trigger another renew.
testCtx.clock.Advance(caRenewInterval)
// Wait until renewer is gone to check database CA contents. This avoids,
// test race condition.
cancel()
require.Eventually(t, func() bool {
select {
case <-caRenewerChan:
return true
default:
return false
}
}, 5*time.Second, time.Second, "failed waiting CA renewer to stop")
// Assert download counts and database CA contents.
require.Equal(t, string(updatedCA), rds.GetStatusCA())
require.Equal(t, int64(2), atomic.LoadInt64(&caDownloader.count))
}
// TestInitAzureCAs given Azure hosted databases, init their CAs ensuring the
// download and get version calls have the correct CA hint.
func TestInitAzureCAs(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
azureDB, err := types.NewDatabaseV3(types.Metadata{
Name: "azure",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
Azure: types.Azure{
Name: "azure",
},
})
require.NoError(t, err)
supportedHints := []string{filepath.Base(azureCAURLBaltimore), filepath.Base(azureCAURLDigiCert)}
initialCA := generateDatabaseCA(t)
caDownloader := &fakeDownloader{
cert: initialCA,
version: []byte("v1"),
assertHintFunc: func(hint string) {
require.Contains(
t,
supportedHints,
hint,
"CA download hint must be one of: %s. But got %q", strings.Join(supportedHints, ","),
hint,
)
},
}
databaseServer := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: []types.Database{azureDB},
NoStart: true,
CADownloader: caDownloader,
})
require.NoError(t, databaseServer.initCACert(ctx, azureDB))
// It must have the contents of two CAs. Since we're returning the same
// content for both, it should have 2 instances of "initialCA".
require.Equal(t, string(bytes.Join([][]byte{initialCA, initialCA}, []byte("\n"))), azureDB.GetStatusCA())
}
func generateDatabaseCA(t *testing.T) []byte {
t.Helper()
_, caCert, err := tlsca.GenerateSelfSignedCA(pkix.Name{
CommonName: "localhost",
}, []string{"localhost"}, defaults.CATTL)
require.NoError(t, err)
return caCert
}
type fakeDownloader struct {
// cert is the cert to return as downloaded one.
cert []byte
// count keeps track of how many times the downloader has been invoked.
count int64
// version is the CA version returned when GetVersion is called.
version []byte
// assertHintFunc is a function used to assert the contents of "hint"
// argument.
assertHintFunc func(string)
}
func (d *fakeDownloader) Download(_ context.Context, _ types.Database, hint string) ([]byte, []byte, error) {
if d.assertHintFunc != nil {
d.assertHintFunc(hint)
}
atomic.AddInt64(&d.count, 1)
return d.cert, d.version, nil
}
func (d *fakeDownloader) GetVersion(_ context.Context, _ types.Database, hint string) ([]byte, error) {
if d.assertHintFunc != nil {
d.assertHintFunc(hint)
}
if d.version == nil {
return nil, trace.NotImplemented("GetVersion not implemented")
}
return d.version, nil
}
type setupTLSTestCfg struct {
commonName string
tlsMode types.DatabaseTLSMode
serverName string
caCert string
injectValidCA bool
}
func setupPostgres(ctx context.Context, t *testing.T, cfg *setupTLSTestCfg) *testContext {
testCtx := setupTestContext(ctx, t)
go testCtx.startProxy()
testCtx.createUserAndRole(ctx, t, "bob", "admin", []string{types.Wildcard}, []string{types.Wildcard})
if cfg.injectValidCA {
cfg.caCert = string(testCtx.databaseCA.GetActiveKeys().TLS[0].Cert)
}
postgresServer, err := postgres.NewTestServer(common.TestServerConfig{
Name: "postgres",
AuthClient: testCtx.authClient,
CN: cfg.commonName,
})
require.NoError(t, err)
go postgresServer.Serve()
t.Cleanup(func() { postgresServer.Close() })
// Offline database server will be tried first and trigger connection error.
postgresDB, err := types.NewDatabaseV3(types.Metadata{
Name: "postgres",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: net.JoinHostPort("localhost", postgresServer.Port()),
TLS: types.DatabaseTLS{
Mode: cfg.tlsMode,
ServerName: cfg.serverName,
CACert: cfg.caCert,
},
})
require.NoError(t, err)
server1 := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: types.Databases{postgresDB},
})
go func() {
for conn := range testCtx.fakeRemoteSite.ProxyConn() {
go server1.HandleConnection(conn)
}
}()
return testCtx
}
func setupMySQL(ctx context.Context, t *testing.T, cfg *setupTLSTestCfg) *testContext {
testCtx := setupTestContext(ctx, t)
go testCtx.startProxy()
testCtx.createUserAndRole(ctx, t, "bob", "admin", []string{types.Wildcard}, []string{types.Wildcard})
if cfg.injectValidCA {
cfg.caCert = string(testCtx.databaseCA.GetActiveKeys().TLS[0].Cert)
}
mysqlServer, err := mysql.NewTestServer(common.TestServerConfig{
Name: "postgres",
AuthClient: testCtx.authClient,
CN: cfg.commonName,
})
require.NoError(t, err)
go mysqlServer.Serve()
t.Cleanup(func() { mysqlServer.Close() })
mysqlDB, err := types.NewDatabaseV3(types.Metadata{
Name: "mysql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: net.JoinHostPort("localhost", mysqlServer.Port()),
TLS: types.DatabaseTLS{
Mode: cfg.tlsMode,
ServerName: cfg.serverName,
CACert: cfg.caCert,
},
})
require.NoError(t, err)
server1 := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: types.Databases{mysqlDB},
})
go func() {
for conn := range testCtx.fakeRemoteSite.ProxyConn() {
go server1.HandleConnection(conn)
}
}()
return testCtx
}
func setupMongo(ctx context.Context, t *testing.T, cfg *setupTLSTestCfg) *testContext {
testCtx := setupTestContext(ctx, t)
go testCtx.startProxy()
testCtx.createUserAndRole(ctx, t, "bob", "admin", []string{types.Wildcard}, []string{types.Wildcard})
if cfg.injectValidCA {
cfg.caCert = string(testCtx.databaseCA.GetActiveKeys().TLS[0].Cert)
}
mongoServer, err := mongodb.NewTestServer(common.TestServerConfig{
Name: "mongo",
AuthClient: testCtx.authClient,
CN: cfg.commonName,
})
require.NoError(t, err)
go mongoServer.Serve()
t.Cleanup(func() {
require.NoError(t, mongoServer.Close())
})
mongoDB, err := types.NewDatabaseV3(types.Metadata{
Name: "mongo",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: net.JoinHostPort("localhost", mongoServer.Port()),
TLS: types.DatabaseTLS{
Mode: cfg.tlsMode,
ServerName: cfg.serverName,
CACert: cfg.caCert,
},
})
require.NoError(t, err)
server1 := testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: types.Databases{mongoDB},
})
t.Cleanup(func() {
require.NoError(t, server1.Close())
})
go func() {
for conn := range testCtx.fakeRemoteSite.ProxyConn() {
go server1.HandleConnection(conn)
}
}()
return testCtx
}
func TestTLSConfiguration(t *testing.T) {
tests := []struct {
// name is the test name
name string
// commonName overrides the DNS name used by the DB.
commonName string
// serverName overrides the certificate DNS name.
serverName string
caCert string
// injectValidCA if true injects a valid CA certificate before connecting to the DB.
injectValidCA bool
tlsMode types.DatabaseTLSMode
// errMsg is an expected error message returned during connection.
errMsg string
}{
{
name: "use default config",
},
{
name: "incorrect server name",
serverName: "abc.example.test",
errMsg: "certificate is valid for localhost, not abc.example.test",
},
{
name: "insecure ignores incorrect CN",
tlsMode: types.DatabaseTLSMode_INSECURE,
commonName: "bad.example.test",
},
{
name: "verify CA ignores incorrect CN",
tlsMode: types.DatabaseTLSMode_VERIFY_CA,
commonName: "bad.example.test",
},
{
name: "verify full fails if CN is incorrect",
tlsMode: types.DatabaseTLSMode_VERIFY_FULL,
commonName: "bad.example.test",
errMsg: "certificate is valid for bad.example.test, not localhost",
},
{
name: "custom domain name with matching server name",
commonName: "customDomain.example.test",
serverName: "customDomain.example.test",
},
{
name: "invalid CA certificate",
caCert: "invalidCert",
errMsg: "invalid server CA certificate",
},
{
name: "valid provided CA",
injectValidCA: true,
},
{
name: "server name can be overridden on provided CA",
injectValidCA: true,
commonName: "testName.example.test",
serverName: "testName.example.test",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Run the same scenario for all supported databases.
for _, dbType := range []string{
defaults.ProtocolPostgres,
defaults.ProtocolMySQL,
defaults.ProtocolMongoDB,
} {
dbType := dbType
t.Run(dbType, func(t *testing.T) {
ctx := context.Background()
cfg := &setupTLSTestCfg{
commonName: tt.commonName,
serverName: tt.serverName,
tlsMode: tt.tlsMode,
caCert: tt.caCert,
}
switch dbType {
case defaults.ProtocolPostgres:
testCtx := setupPostgres(ctx, t, cfg)
t.Cleanup(func() {
err := testCtx.Close()
require.NoError(t, err)
})
psql, err := testCtx.postgresClient(ctx, "bob", "postgres", "postgres", "postgres")
if tt.errMsg == "" {
require.NoError(t, err)
err = psql.Close(ctx)
require.NoError(t, err)
} else {
require.Error(t, err)
// skip error message validation here. Postgres driver by default tried to connect to
// a database on localhost using IPv4 and IPv6. Docker doesn't enable IPv6 support
// by default and that fails the check (on our CI and every default Docker installation )
// as error is different "connection refused" that expected x509 related.
}
case defaults.ProtocolMySQL:
testCtx := setupMySQL(ctx, t, cfg)
t.Cleanup(func() {
err := testCtx.Close()
require.NoError(t, err)
})
mysqlConn, err := testCtx.mysqlClient("bob", "mysql", "admin")
if tt.errMsg == "" {
require.NoError(t, err)
err = mysqlConn.Close()
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMsg)
}
case defaults.ProtocolMongoDB:
testCtx := setupMongo(ctx, t, cfg)
t.Cleanup(func() {
err := testCtx.Close()
require.NoError(t, err)
})
mongoConn, err := testCtx.mongoClient(ctx, "bob", "mongo", "admin")
t.Cleanup(func() {
err = mongoConn.Disconnect(ctx)
require.NoError(t, err)
})
if tt.errMsg == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
// Do not verify Mongo error message. On authentication error Mongo re-tries and
// returns timeout instead of x509 related error.
}
default:
t.Fatalf("unrecognized database: %s", dbType)
}
})
}
})
}
}
func TestRDSCAURLForDatabase(t *testing.T) {
tests := map[string]string{
"us-west-1": "https://truststore.pki.rds.amazonaws.com/us-west-1/us-west-1-bundle.pem",
"ca-central-1": "https://truststore.pki.rds.amazonaws.com/ca-central-1/ca-central-1-bundle.pem",
"us-gov-east-1": "https://truststore.pki.us-gov-west-1.rds.amazonaws.com/us-gov-east-1/us-gov-east-1-bundle.pem",
"us-gov-west-1": "https://truststore.pki.us-gov-west-1.rds.amazonaws.com/us-gov-west-1/us-gov-west-1-bundle.pem",
"cn-northwest-1": "https://rds-truststore.s3.cn-north-1.amazonaws.com.cn/cn-northwest-1/cn-northwest-1-bundle.pem",
"cn-north-1": "https://rds-truststore.s3.cn-north-1.amazonaws.com.cn/cn-north-1/cn-north-1-bundle.pem",
}
for region, expectURL := range tests {
t.Run(region, func(t *testing.T) {
database, err := types.NewDatabaseV3(types.Metadata{
Name: "db",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: region},
})
require.NoError(t, err)
require.Equal(t, expectURL, rdsCAURLForDatabase(database))
})
}
}
func TestRedshiftCAURLForDatabase(t *testing.T) {
tests := map[string]string{
"us-west-1": "https://s3.amazonaws.com/redshift-downloads/amazon-trust-ca-bundle.crt",
"ca-central-1": "https://s3.amazonaws.com/redshift-downloads/amazon-trust-ca-bundle.crt",
"cn-north-1": "https://s3.cn-north-1.amazonaws.com.cn/redshift-downloads-cn/amazon-trust-ca-bundle.crt",
"cn-northwest-1": "https://s3.cn-north-1.amazonaws.com.cn/redshift-downloads-cn/amazon-trust-ca-bundle.crt",
}
for region, expectURL := range tests {
t.Run(region, func(t *testing.T) {
database, err := types.NewDatabaseV3(types.Metadata{
Name: "db",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: region},
})
require.NoError(t, err)
require.Equal(t, expectURL, redshiftCAURLForDatabase(database))
})
}
}
func TestCADownloaderGetVersion(t *testing.T) {
ctx := context.Background()
// Given databases that have CA certs downloaded from a public CDN, the
// downloader should return the CA version or a not implemented error.
t.Run("from URL", func(t *testing.T) {
t.Parallel()
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
for _, tc := range []struct {
desc string
database types.Database
hint string
version []byte
supportEtag bool
expectError require.ErrorAssertionFunc
expectedHTTPCalls int32
}{
{
desc: "support to ETag",
database: rds,
version: []byte("rds-test-cert"),
supportEtag: true,
expectError: require.NoError,
expectedHTTPCalls: 1,
},
{
desc: "without support to ETag returns error",
database: rds,
supportEtag: false,
expectError: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.True(t, trace.IsNotImplemented(err), "expected trace.NotImplementedError but received %T", err)
},
expectedHTTPCalls: 1,
},
} {
t.Run(tc.desc, func(t *testing.T) {
var httpCalls int32
// Start the CA CDN server.
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tc.supportEtag {
w.Header().Set("ETag", string(tc.version))
}
w.WriteHeader(http.StatusOK)
atomic.AddInt32(&httpCalls, 1)
}))
defer ts.Close()
downloader := &realDownloader{
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// Replace DialContext to always hit the test server.
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, strings.TrimPrefix(ts.URL, "https://"))
},
},
},
}
version, err := downloader.GetVersion(ctx, tc.database, "")
tc.expectError(t, err)
require.Equal(t, tc.version, version)
require.Equal(t, tc.expectedHTTPCalls, atomic.LoadInt32(&httpCalls))
})
}
})
// Given a CloudSQL database, the downloader should return an error because
// it is not supported.
t.Run("from CloudSQL", func(t *testing.T) {
cloudSQL, err := types.NewDatabaseV3(types.Metadata{
Name: "cloud-sql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
GCP: types.GCPCloudSQL{InstanceID: "instance-id", ProjectID: "project-id"},
})
require.NoError(t, err)
downloader := &realDownloader{}
_, err = downloader.GetVersion(ctx, cloudSQL, "")
require.Error(t, err)
require.True(t, trace.IsNotImplemented(err), "expected trace.NotImplementedError but received %T", err)
})
}
func TestCADownloaderDownload(t *testing.T) {
ctx := context.Background()
// Given databases that have CA certs downloaded from a public CDN, the
// downloader should return the CA contents and version.
t.Run("from URL", func(t *testing.T) {
t.Parallel()
rds, err := types.NewDatabaseV3(types.Metadata{
Name: "rds",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{Region: "us-east-1"},
})
require.NoError(t, err)
// Calculate the resource version using sha256 of the CA cert.
certContents := []byte("rds-test-cert")
sum := sha256.Sum256(certContents)
for _, tc := range []struct {
desc string
database types.Database
cert []byte
hint string
version []byte
supportEtag bool
statusCode int
expectError require.ErrorAssertionFunc
expectedHTTPCalls int32
}{
{
desc: "version from ETag",
database: rds,
cert: []byte("rds-test-cert"),
version: []byte("rds-test-version"),
supportEtag: true,
statusCode: http.StatusOK,
expectError: require.NoError,
expectedHTTPCalls: 1,
},
{
desc: "version from contents",
database: rds,
cert: certContents,
version: sum[:],
supportEtag: false,
statusCode: http.StatusOK,
expectError: require.NoError,
expectedHTTPCalls: 1,
},
{
desc: "download failure",
database: rds,
supportEtag: false,
statusCode: http.StatusInternalServerError,
expectError: require.Error,
expectedHTTPCalls: 1,
},
} {
t.Run(tc.desc, func(t *testing.T) {
var httpCalls int32
// Start the CA CDN server.
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tc.supportEtag {
w.Header().Set("ETag", string(tc.version))
}
w.WriteHeader(tc.statusCode)
w.Write(tc.cert)
atomic.AddInt32(&httpCalls, 1)
}))
defer ts.Close()
downloader := &realDownloader{
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// Replace DialContext to always hit the test server.
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, strings.TrimPrefix(ts.URL, "https://"))
},
},
},
}
contents, version, err := downloader.Download(ctx, tc.database, "")
tc.expectError(t, err)
require.Equal(t, tc.cert, contents)
require.Equal(t, tc.version, version)
require.Equal(t, tc.expectedHTTPCalls, atomic.LoadInt32(&httpCalls))
})
}
})
// Given a CloudSQL database, the downloader should return the database
// certificate.
t.Run("from CloudSQL", func(t *testing.T) {
cloudSQL, err := types.NewDatabaseV3(types.Metadata{
Name: "cloud-sql",
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
GCP: types.GCPCloudSQL{InstanceID: "instance-id", ProjectID: "project-id"},
})
require.NoError(t, err)
certContents := []byte("cloud-sql-test-cert")
fingerPrint := []byte("cert-fingerprint")
downloader := &realDownloader{
sqlAdminClient: &mocks.GCPSQLAdminClientMock{
DatabaseInstance: &sqladmin.DatabaseInstance{
ServerCaCert: &sqladmin.SslCert{
Cert: string(certContents),
Sha1Fingerprint: string(fingerPrint),
},
},
},
}
contents, version, err := downloader.Download(ctx, cloudSQL, "")
require.NoError(t, err)
require.Equal(t, certContents, contents)
require.Equal(t, fingerPrint, version)
})
}