Files
teleport/integration/db/db_integration_test.go
T
Jeff Pihach 1513294b7b Add CreatedBy and CreatedOn fields to locks. (#22667)
* Add CreatedBy and CreatedOn fields to locks.

* Move the setting of the CreatedBy and CreatedOn for locks to the server instead of the client.

* Add tests for the tctl resource lock import and for the tctl lock create command.

* Use clockwork for time in lock creation tests.

* Make fake clock optional and use the golang 0 date.

* Change CreatedOn to CreatedAt.

* Allow a custom time to be passed through as a fake clock.

* Actually accept a fake clock with useFakeClock.

* Pass Clock through as a configuration value when initializing Teleport for the tests.

* Test cleanup

* Set CreatedBy and CreatedAt values in test mocks.

* Fix failing test from enabling clock passthrough to auth.
2023-03-13 16:14:24 +00:00

1026 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Copyright 2020-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 (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/go-mysql-org/go-mysql/client"
"github.com/gravitational/trace"
"github.com/jackc/pgconn"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db"
"github.com/gravitational/teleport/lib/srv/db/cassandra"
"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"
)
// TestDatabaseAccess runs the database access integration test suite.
//
// It allows to make the entire cluster set up once, instead of per test,
// which speeds things up significantly.
func TestDatabaseAccess(t *testing.T) {
pack := SetupDatabaseTest(t,
// set tighter rotation intervals
WithLeafConfig(func(config *servicecfg.Config) {
config.PollingPeriod = 5 * time.Second
config.RotationConnectionInterval = 2 * time.Second
}),
WithRootConfig(func(config *servicecfg.Config) {
config.PollingPeriod = 5 * time.Second
config.RotationConnectionInterval = 2 * time.Second
}),
)
pack.WaitForLeaf(t)
t.Run("PostgresRootCluster", pack.testPostgresRootCluster)
t.Run("PostgresLeafCluster", pack.testPostgresLeafCluster)
t.Run("MySQLRootCluster", pack.testMySQLRootCluster)
t.Run("MySQLLeafCluster", pack.testMySQLLeafCluster)
t.Run("MongoRootCluster", pack.testMongoRootCluster)
t.Run("MongoLeafCluster", pack.testMongoLeafCluster)
t.Run("MongoConnectionCount", pack.testMongoConnectionCount)
t.Run("HARootCluster", pack.testHARootCluster)
t.Run("HALeafCluster", pack.testHALeafCluster)
t.Run("LargeQuery", pack.testLargeQuery)
t.Run("AgentState", pack.testAgentState)
t.Run("CassandraRootCluster", pack.testCassandraRootCluster)
t.Run("CassandraLeafCluster", pack.testCassandraLeafCluster)
t.Run("IPPinning", pack.testIPPinning)
// This test should go last because it rotates the Database CA.
t.Run("RotateTrustedCluster", pack.testRotateTrustedCluster)
}
// TestDatabaseAccessSeparateListeners tests the Mongo and Postgres separate port setup.
func TestDatabaseAccessSeparateListeners(t *testing.T) {
pack := SetupDatabaseTest(t,
WithListenerSetupDatabaseTest(helpers.SeparateMongoAndPostgresPortSetup),
)
t.Run("PostgresSeparateListener", pack.testPostgresSeparateListener)
t.Run("MongoSeparateListener", pack.testMongoSeparateListener)
}
// testIPPinning tests a scenario where a user with IP pinning
// connects to a database
func (p *DatabasePack) testIPPinning(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestBuildType: modules.BuildEnterprise,
TestFeatures: modules.Features{DB: true},
})
type testCase struct {
desc string
targetCluster databaseClusterPack
pinnedIP string
wantClientErr string
}
testCases := []testCase{
{
desc: "root cluster, no pinned ip",
targetCluster: p.Root,
},
{
desc: "root cluster, correct pinned ip",
targetCluster: p.Root,
pinnedIP: "127.0.0.1",
},
{
desc: "root cluster, incorrect pinned ip",
targetCluster: p.Root,
wantClientErr: "pinned IP doesn't match observed client IP",
pinnedIP: "127.0.0.2",
},
{
desc: "leaf cluster, no pinned ip",
targetCluster: p.Leaf,
},
{
desc: "leaf cluster, correct pinned ip",
targetCluster: p.Leaf,
pinnedIP: "127.0.0.1",
},
{
desc: "leaf cluster, incorrect pinned ip",
targetCluster: p.Leaf,
wantClientErr: "pinned IP doesn't match observed client IP",
pinnedIP: "127.0.0.2",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// Connect to the database service in root cluster.
testClient, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: tc.targetCluster.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
PinnedIP: tc.pinnedIP,
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: tc.targetCluster.PostgresService.Name,
Protocol: tc.targetCluster.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
if tc.wantClientErr != "" {
require.ErrorContains(t, err, tc.wantClientErr)
return
}
require.NoError(t, err)
wantQueryCount := tc.targetCluster.postgres.QueryCount() + 1
// Execute a query.
result, err := testClient.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantQueryCount, tc.targetCluster.postgres.QueryCount())
// Disconnect.
err = testClient.Close(context.Background())
require.NoError(t, err)
})
}
}
// testPostgresRootCluster tests a scenario where a user connects
// to a Postgres database running in a root cluster.
func (p *DatabasePack) testPostgresRootCluster(t *testing.T) {
// Connect to the database service in root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.PostgresService.Name,
Protocol: p.Root.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.postgres.QueryCount() + 1
wantLeafQueryCount := p.Leaf.postgres.QueryCount()
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
// testPostgresLeafCluster tests a scenario where a user connects
// to a Postgres database running in a leaf cluster via a root cluster.
func (p *DatabasePack) testPostgresLeafCluster(t *testing.T) {
// Connect to the database service in leaf cluster via root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web, // Connecting via root cluster.
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.PostgresService.Name,
Protocol: p.Leaf.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.postgres.QueryCount()
wantLeafQueryCount := p.Leaf.postgres.QueryCount() + 1
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
func (p *DatabasePack) testRotateTrustedCluster(t *testing.T) {
// TODO(jakule): Fix flaky test
t.Skip("flaky test, skip for now")
var (
ctx = context.Background()
rootCluster = p.Root.Cluster
authServer = rootCluster.Process.GetAuthServer()
clusterRootName = rootCluster.Secrets.SiteName
clusterLeafName = p.Leaf.Cluster.Secrets.SiteName
)
pw := phaseWatcher{
clusterRootName: clusterRootName,
pollingPeriod: rootCluster.Process.Config.PollingPeriod,
clock: p.clock,
siteAPI: rootCluster.GetSiteAPI(clusterLeafName),
certType: types.DatabaseCA,
}
currentDbCA, err := p.Root.dbAuthClient.GetCertAuthority(ctx, types.CertAuthID{
Type: types.DatabaseCA,
DomainName: clusterRootName,
}, false)
require.NoError(t, err)
rotationPhases := []string{
types.RotationPhaseInit, types.RotationPhaseUpdateClients,
types.RotationPhaseUpdateServers, types.RotationPhaseStandby,
}
waitForEvent := func(process *service.TeleportProcess, event string) {
_, err := process.WaitForEventTimeout(20*time.Second, event)
require.NoError(t, err, "timeout waiting for service to broadcast event %s", event)
}
for _, phase := range rotationPhases {
errChan := make(chan error, 1)
go func() {
errChan <- pw.waitForPhase(phase, func() error {
return authServer.RotateCertAuthority(ctx, auth.RotateRequest{
Type: types.DatabaseCA,
TargetPhase: phase,
Mode: types.RotationModeManual,
})
})
}()
err = <-errChan
if err != nil && strings.Contains(err.Error(), "context deadline exceeded") {
// TODO(jakule): Workaround for CertAuthorityWatcher failing to get the correct rotation status.
// Query auth server directly to see if the incorrect rotation status is a rotation or watcher problem.
dbCA, err := p.Leaf.Cluster.Process.GetAuthServer().GetCertAuthority(ctx, types.CertAuthID{
Type: types.DatabaseCA,
DomainName: clusterRootName,
}, false)
require.NoError(t, err)
require.Equal(t, dbCA.GetRotation().Phase, phase)
} else {
require.NoError(t, err)
}
// Reload doesn't happen on Init
if phase == types.RotationPhaseInit {
continue
}
waitForEvent(p.Root.Cluster.Process, service.TeleportReloadEvent)
waitForEvent(p.Leaf.Cluster.Process, service.TeleportReadyEvent)
p.WaitForLeaf(t)
}
rotatedDbCA, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
Type: types.DatabaseCA,
DomainName: clusterRootName,
}, false)
require.NoError(t, err)
// Sanity check. Check if the CA was rotated.
require.NotEqual(t, currentDbCA.GetActiveKeys(), rotatedDbCA.GetActiveKeys())
// Connect to the database service in leaf cluster via root cluster.
dbClient, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web, // Connecting via root cluster.
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.PostgresService.Name,
Protocol: p.Leaf.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantLeafQueryCount := p.Leaf.postgres.QueryCount() + 1
wantRootQueryCount := p.Root.postgres.QueryCount()
result, err := dbClient.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
// Disconnect.
err = dbClient.Close(context.Background())
require.NoError(t, err)
}
// phaseWatcher holds all arguments required by rotation watcher.
type phaseWatcher struct {
clusterRootName string
pollingPeriod time.Duration
clock clockwork.Clock
siteAPI types.Events
certType types.CertAuthType
}
// waitForPhase waits until rootCluster cluster detects the rotation. fn is a rotation function that is called after
// watcher is created.
func (p *phaseWatcher) waitForPhase(phase string, fn func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), p.pollingPeriod*10)
defer cancel()
watcher, err := services.NewCertAuthorityWatcher(ctx, services.CertAuthorityWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: teleport.ComponentProxy,
Clock: p.clock,
Client: p.siteAPI,
},
Types: []types.CertAuthType{p.certType},
})
if err != nil {
return err
}
defer watcher.Close()
if err := fn(); err != nil {
return trace.Wrap(err)
}
sub, err := watcher.Subscribe(ctx, types.CertAuthorityFilter{
p.certType: p.clusterRootName,
})
if err != nil {
return trace.Wrap(err)
}
defer sub.Close()
var lastPhase string
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return trace.CompareFailed("failed to converge to phase %q, last phase %q certType: %v err: %v", phase, lastPhase, p.certType, ctx.Err())
case <-sub.Done():
return trace.CompareFailed("failed to converge to phase %q, last phase %q certType: %v err: %v", phase, lastPhase, p.certType, sub.Error())
case evt := <-sub.Events():
switch evt.Type {
case types.OpPut:
ca, ok := evt.Resource.(types.CertAuthority)
if !ok {
return trace.BadParameter("expected a ca got type %T", evt.Resource)
}
if ca.GetRotation().Phase == phase {
return nil
}
lastPhase = ca.GetRotation().Phase
}
}
}
return trace.CompareFailed("failed to converge to phase %q, last phase %q", phase, lastPhase)
}
// testMySQLRootCluster tests a scenario where a user connects
// to a MySQL database running in a root cluster.
func (p *DatabasePack) testMySQLRootCluster(t *testing.T) {
// Connect to the database service in root cluster.
client, err := mysql.MakeTestClient(common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.MySQL,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.MysqlService.Name,
Protocol: p.Root.MysqlService.Protocol,
Username: "root",
// With MySQL database name doesn't matter as it's not subject to RBAC atm.
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.mysql.QueryCount() + 1
wantLeafQueryCount := p.Leaf.mysql.QueryCount()
// Execute a query.
result, err := client.Execute("select 1")
require.NoError(t, err)
require.Equal(t, mysql.TestQueryResponse, result)
require.Equal(t, wantRootQueryCount, p.Root.mysql.QueryCount())
require.Equal(t, wantLeafQueryCount, p.Leaf.mysql.QueryCount())
// Disconnect.
err = client.Close()
require.NoError(t, err)
}
// testMySQLLeafCluster tests a scenario where a user connects
// to a MySQL database running in a leaf cluster via a root cluster.
func (p *DatabasePack) testMySQLLeafCluster(t *testing.T) {
// Connect to the database service in leaf cluster via root cluster.
client, err := mysql.MakeTestClient(common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.MySQL, // Connecting via root cluster.
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.MysqlService.Name,
Protocol: p.Leaf.MysqlService.Protocol,
Username: "root",
// With MySQL database name doesn't matter as it's not subject to RBAC atm.
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.mysql.QueryCount()
wantLeafQueryCount := p.Leaf.mysql.QueryCount() + 1
// Execute a query.
result, err := client.Execute("select 1")
require.NoError(t, err)
require.Equal(t, mysql.TestQueryResponse, result)
require.Equal(t, wantLeafQueryCount, p.Leaf.mysql.QueryCount())
require.Equal(t, wantRootQueryCount, p.Root.mysql.QueryCount())
// Disconnect.
err = client.Close()
require.NoError(t, err)
}
// testMongoRootCluster tests a scenario where a user connects
// to a Mongo database running in a root cluster.
func (p *DatabasePack) testMongoRootCluster(t *testing.T) {
// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.MongoService.Name,
Protocol: p.Root.MongoService.Protocol,
Username: "admin",
},
})
require.NoError(t, err)
// Execute a query.
_, err = client.Database("test").Collection("test").Find(context.Background(), bson.M{})
require.NoError(t, err)
// Disconnect.
err = client.Disconnect(context.Background())
require.NoError(t, err)
}
// testMongoConnectionCount tests if mongo service releases
// resource after a mongo client disconnect.
func (p *DatabasePack) testMongoConnectionCount(t *testing.T) {
connectMongoClient := func(t *testing.T) (serverConnectionCount int32) {
// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.MongoService.Name,
Protocol: p.Root.MongoService.Protocol,
Username: "admin",
},
})
require.NoError(t, err)
// Execute a query.
_, err = client.Database("test").Collection("test").Find(context.Background(), bson.M{})
require.NoError(t, err)
// Get a server connection count before disconnect.
serverConnectionCount = p.Root.mongo.GetActiveConnectionsCount()
// Disconnect.
err = client.Disconnect(context.Background())
require.NoError(t, err)
return serverConnectionCount
}
// Get connection count while the first client is connected.
initialConnectionCount := connectMongoClient(t)
// Check if active connections count is not growing over time when new
// clients connect to the mongo server.
clientCount := 8
for i := 0; i < clientCount; i++ {
// Note that connection count per client fluctuates between 6 and 9.
// Use InDelta to avoid flaky test.
require.InDelta(t, initialConnectionCount, connectMongoClient(t), 3)
}
// Wait until the server reports no more connections. This usually happens
// really quick but wait a little longer just in case.
waitUntilNoConnections := func() bool {
return p.Root.mongo.GetActiveConnectionsCount() == 0
}
require.Eventually(t, waitUntilNoConnections, 5*time.Second, 100*time.Millisecond)
}
// testMongoLeafCluster tests a scenario where a user connects
// to a Mongo database running in a leaf cluster.
func (p *DatabasePack) testMongoLeafCluster(t *testing.T) {
// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web, // Connecting via root cluster.
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.MongoService.Name,
Protocol: p.Leaf.MongoService.Protocol,
Username: "admin",
},
})
require.NoError(t, err)
// Execute a query.
_, err = client.Database("test").Collection("test").Find(context.Background(), bson.M{})
require.NoError(t, err)
// Disconnect.
err = client.Disconnect(context.Background())
require.NoError(t, err)
}
// TestRootLeafIdleTimeout tests idle client connection termination by proxy and DB services in
// trusted cluster setup.
func TestDatabaseRootLeafIdleTimeout(t *testing.T) {
clock := clockwork.NewFakeClockAt(time.Now())
pack := SetupDatabaseTest(t, WithClock(clock))
pack.WaitForLeaf(t)
var (
rootAuthServer = pack.Root.Cluster.Process.GetAuthServer()
rootRole = pack.Root.role
leafAuthServer = pack.Leaf.Cluster.Process.GetAuthServer()
leafRole = pack.Leaf.role
idleTimeout = time.Minute
)
rootAuthServer.SetClock(clockwork.NewFakeClockAt(time.Now()))
leafAuthServer.SetClock(clockwork.NewFakeClockAt(time.Now()))
mkMySQLLeafDBClient := func(t *testing.T) *client.Conn {
// Connect to the database service in leaf cluster via root cluster.
client, err := mysql.MakeTestClient(common.TestClientConfig{
AuthClient: pack.Root.Cluster.GetSiteAPI(pack.Root.Cluster.Secrets.SiteName),
AuthServer: pack.Root.Cluster.Process.GetAuthServer(),
Address: pack.Root.Cluster.MySQL, // Connecting via root cluster.
Cluster: pack.Leaf.Cluster.Secrets.SiteName,
Username: pack.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: pack.Leaf.MysqlService.Name,
Protocol: pack.Leaf.MysqlService.Protocol,
Username: "root",
},
})
require.NoError(t, err)
return client
}
t.Run("root role without idle timeout", func(t *testing.T) {
client := mkMySQLLeafDBClient(t)
_, err := client.Execute("select 1")
require.NoError(t, err)
clock.Advance(idleTimeout)
_, err = client.Execute("select 1")
require.NoError(t, err)
err = client.Close()
require.NoError(t, err)
})
t.Run("root role with idle timeout", func(t *testing.T) {
setRoleIdleTimeout(t, rootAuthServer, rootRole, idleTimeout)
require.Eventually(t, func() bool {
role, err := rootAuthServer.GetRole(context.Background(), rootRole.GetName())
assert.NoError(t, err)
return time.Duration(role.GetOptions().ClientIdleTimeout) == idleTimeout
}, time.Second, time.Millisecond*100, "role idle timeout propagation filed")
client := mkMySQLLeafDBClient(t)
_, err := client.Execute("select 1")
require.NoError(t, err)
now := clock.Now()
clock.Advance(idleTimeout)
helpers.WaitForAuditEventTypeWithBackoff(t, pack.Root.Cluster.Process.GetAuthServer(), now, events.ClientDisconnectEvent)
_, err = client.Execute("select 1")
require.Error(t, err)
setRoleIdleTimeout(t, rootAuthServer, rootRole, time.Hour)
})
t.Run("leaf role with idle timeout", func(t *testing.T) {
setRoleIdleTimeout(t, leafAuthServer, leafRole, idleTimeout)
require.Eventually(t, func() bool {
role, err := leafAuthServer.GetRole(context.Background(), leafRole.GetName())
assert.NoError(t, err)
return time.Duration(role.GetOptions().ClientIdleTimeout) == idleTimeout
}, time.Second, time.Millisecond*100, "role idle timeout propagation filed")
client := mkMySQLLeafDBClient(t)
_, err := client.Execute("select 1")
require.NoError(t, err)
now := clock.Now()
clock.Advance(idleTimeout)
helpers.WaitForAuditEventTypeWithBackoff(t, pack.Leaf.Cluster.Process.GetAuthServer(), now, events.ClientDisconnectEvent)
_, err = client.Execute("select 1")
require.Error(t, err)
setRoleIdleTimeout(t, leafAuthServer, leafRole, time.Hour)
})
}
// TestDatabaseAccessUnspecifiedHostname tests DB agent reverse tunnel connection in case where host address is
// unspecified thus is not present in the valid principal list. The DB agent should replace unspecified address (0.0.0.0)
// with localhost and successfully establish reverse tunnel connection.
func TestDatabaseAccessUnspecifiedHostname(t *testing.T) {
pack := SetupDatabaseTest(t,
WithNodeName("0.0.0.0"),
)
// Connect to the database service in root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: pack.Root.Cluster.GetSiteAPI(pack.Root.Cluster.Secrets.SiteName),
AuthServer: pack.Root.Cluster.Process.GetAuthServer(),
Address: pack.Root.Cluster.Web,
Cluster: pack.Root.Cluster.Secrets.SiteName,
Username: pack.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: pack.Root.PostgresService.Name,
Protocol: pack.Root.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, uint32(1), pack.Root.postgres.QueryCount())
require.Equal(t, uint32(0), pack.Leaf.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
func (p *DatabasePack) testPostgresSeparateListener(t *testing.T) {
// Connect to the database service in root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Postgres,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.PostgresService.Name,
Protocol: p.Root.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.postgres.QueryCount() + 1
wantLeafQueryCount := p.Root.postgres.QueryCount()
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
// TestDatabaseAccessPostgresSeparateListener tests postgres proxy listener running on separate port
// with DisableTLS.
func TestDatabaseAccessPostgresSeparateListenerTLSDisabled(t *testing.T) {
pack := SetupDatabaseTest(t,
WithListenerSetupDatabaseTest(helpers.SeparatePostgresPortSetup),
WithRootConfig(func(config *servicecfg.Config) {
config.Proxy.DisableTLS = true
}),
)
pack.testPostgresSeparateListener(t)
}
func init() {
// Override database agents shuffle behavior to ensure they're always
// tried in the same order during tests. Used for HA tests.
db.SetShuffleFunc(db.ShuffleSort)
}
// testHARootCluster verifies that proxy falls back to a healthy
// database agent when multiple agents are serving the same database and one
// of them is down in a root cluster.
func (p *DatabasePack) testHARootCluster(t *testing.T) {
// Insert a database server entry not backed by an actual running agent
// to simulate a scenario when an agent is down but the resource hasn't
// expired from the backend yet.
dbServer, err := types.NewDatabaseServerV3(types.Metadata{
Name: p.Root.PostgresService.Name,
}, types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: p.Root.postgresAddr,
// To make sure unhealthy server is always picked in tests first, make
// sure its host ID always compares as "smaller" as the tests sort
// agents.
HostID: "0000",
Hostname: "test",
})
require.NoError(t, err)
_, err = p.Root.Cluster.Process.GetAuthServer().UpsertDatabaseServer(
context.Background(), dbServer)
require.NoError(t, err)
// Connect to the database service in root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.PostgresService.Name,
Protocol: p.Root.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.postgres.QueryCount() + 1
wantLeafQueryCount := p.Leaf.postgres.QueryCount()
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
// testHALeafCluster verifies that proxy falls back to a healthy
// database agent when multiple agents are serving the same database and one
// of them is down in a leaf cluster.
func (p *DatabasePack) testHALeafCluster(t *testing.T) {
// Insert a database server entry not backed by an actual running agent
// to simulate a scenario when an agent is down but the resource hasn't
// expired from the backend yet.
dbServer, err := types.NewDatabaseServerV3(types.Metadata{
Name: p.Leaf.PostgresService.Name,
}, types.DatabaseServerSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: p.Leaf.postgresAddr,
// To make sure unhealthy server is always picked in tests first, make
// sure its host ID always compares as "smaller" as the tests sort
// agents.
HostID: "0000",
Hostname: "test",
})
require.NoError(t, err)
_, err = p.Leaf.Cluster.Process.GetAuthServer().UpsertDatabaseServer(
context.Background(), dbServer)
require.NoError(t, err)
// Connect to the database service in leaf cluster via root cluster.
client, err := postgres.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web, // Connecting via root cluster.
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.PostgresService.Name,
Protocol: p.Leaf.PostgresService.Protocol,
Username: "postgres",
Database: "test",
},
})
require.NoError(t, err)
wantRootQueryCount := p.Root.postgres.QueryCount()
wantLeafQueryCount := p.Leaf.postgres.QueryCount() + 1
// Execute a query.
result, err := client.Exec(context.Background(), "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
require.Equal(t, wantLeafQueryCount, p.Leaf.postgres.QueryCount())
require.Equal(t, wantRootQueryCount, p.Root.postgres.QueryCount())
// Disconnect.
err = client.Close(context.Background())
require.NoError(t, err)
}
// testDatabaseAccessMongoSeparateListener tests mongo proxy listener running on separate port.
func (p *DatabasePack) testMongoSeparateListener(t *testing.T) {
// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Mongo,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.MongoService.Name,
Protocol: p.Root.MongoService.Protocol,
Username: "admin",
},
})
require.NoError(t, err)
// Execute a query.
_, err = client.Database("test").Collection("test").Find(context.Background(), bson.M{})
require.NoError(t, err)
// Disconnect.
err = client.Disconnect(context.Background())
require.NoError(t, err)
}
func (p *DatabasePack) testAgentState(t *testing.T) {
tests := map[string]struct {
agentParams databaseAgentStartParams
}{
"WithStaticDatabases": {
agentParams: databaseAgentStartParams{
databases: []servicecfg.Database{
{Name: "mysql", Protocol: defaults.ProtocolMySQL, URI: "localhost:3306"},
{Name: "pg", Protocol: defaults.ProtocolPostgres, URI: "localhost:5432"},
},
},
},
"WithResourceMatchers": {
agentParams: databaseAgentStartParams{
resourceMatchers: []services.ResourceMatcher{
{Labels: types.Labels{"*": []string{"*"}}},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Start also ensures that the database agent has the “ready” state.
// If the agent can’t make it, this function will fail the test.
agent, _ := p.startRootDatabaseAgent(t, test.agentParams)
// In addition to the checks performed during the agent start,
// we’ll request the diagnostic server to ensure the readyz route
// is returning to the proper state.
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%v/readyz", agent.Config.DiagnosticAddr.Addr), nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, string(respBody))
})
}
}
// testCassandraRootCluster tests a scenario where a user connects
// to a Cassandra database running in a root cluster.
func (p *DatabasePack) testCassandraRootCluster(t *testing.T) {
// Connect to the database service in root cluster.
dbConn, err := cassandra.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Root.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Root.CassandraService.Name,
Protocol: p.Root.CassandraService.Protocol,
Username: "cassandra",
},
})
require.NoError(t, err)
var clusterName string
err = dbConn.Query("select cluster_name from system.local").Scan(&clusterName)
require.NoError(t, err)
require.Equal(t, "Test Cluster", clusterName)
dbConn.Close()
}
// testCassandraLeafCluster tests a scenario where a user connects
// to a Cassandra database running in a root cluster.
func (p *DatabasePack) testCassandraLeafCluster(t *testing.T) {
// Connect to the database service in root cluster.
dbConn, err := cassandra.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: p.Root.Cluster.GetSiteAPI(p.Root.Cluster.Secrets.SiteName),
AuthServer: p.Root.Cluster.Process.GetAuthServer(),
Address: p.Root.Cluster.Web,
Cluster: p.Leaf.Cluster.Secrets.SiteName,
Username: p.Root.User.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: p.Leaf.CassandraService.Name,
Protocol: p.Leaf.CassandraService.Protocol,
Username: "cassandra",
},
})
require.NoError(t, err)
var clusterName string
err = dbConn.Query("select cluster_name from system.local").Scan(&clusterName)
require.NoError(t, err)
require.Equal(t, "Test Cluster", clusterName)
dbConn.Close()
}
func setRoleIdleTimeout(t *testing.T, authServer *auth.Server, role types.Role, idleTimout time.Duration) {
opts := role.GetOptions()
opts.ClientIdleTimeout = types.Duration(idleTimout)
role.SetOptions(opts)
err := authServer.UpsertRole(context.Background(), role)
require.NoError(t, err)
}