Files
teleport/integration/db/fixture.go
T
rosstimothy b0dc78610e Migrate upserting CAs to gRPC (#23140)
Adds `UpsertCertAuthority` to the `teleport.v1.trust.TrustService`
and deprecates the HTTP api. The `services.Trust` interface was
also updated to include a `context.Context` in both
`CreateCertAuthority` and `UpsertCertAuthority.

To ensure backwards compatibility the `auth.Client` will first
attempt to use the gRPC client and if that fails with a not
implemented error it will fallback to using HTTP.

Part of #6394
2023-03-16 14:46:43 +00:00

505 lines
15 KiB
Go

// Copyright 2022 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"
"net"
"path/filepath"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/breaker"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"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/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"
"github.com/gravitational/teleport/lib/utils"
)
type DatabasePack struct {
Root databaseClusterPack
Leaf databaseClusterPack
clock clockwork.Clock
}
type databaseClusterPack struct {
Cluster *helpers.TeleInstance
User types.User
role types.Role
dbProcess *service.TeleportProcess
dbAuthClient *auth.Client
PostgresService servicecfg.Database
postgresAddr string
postgres *postgres.TestServer
MysqlService servicecfg.Database
mysqlAddr string
mysql *mysql.TestServer
MongoService servicecfg.Database
mongoAddr string
mongo *mongodb.TestServer
name string
CassandraService servicecfg.Database
cassandra *cassandra.TestServer
cassandraAddr string
}
func mustListen(t *testing.T) (net.Listener, string) {
t.Helper()
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
_, port, err := net.SplitHostPort(listener.Addr().String())
require.NoError(t, err)
return listener, net.JoinHostPort("localhost", port)
}
func (pack *databaseClusterPack) StartDatabaseServices(t *testing.T, clock clockwork.Clock) {
var err error
var postgresListener, mysqlListener, mongoListener, cassandaListener net.Listener
postgresListener, pack.postgresAddr = mustListen(t)
pack.PostgresService = servicecfg.Database{
Name: fmt.Sprintf("%s-postgres", pack.name),
Protocol: defaults.ProtocolPostgres,
URI: pack.postgresAddr,
}
mysqlListener, pack.mysqlAddr = mustListen(t)
pack.MysqlService = servicecfg.Database{
Name: fmt.Sprintf("%s-mysql", pack.name),
Protocol: defaults.ProtocolMySQL,
URI: pack.mysqlAddr,
}
mongoListener, pack.mongoAddr = mustListen(t)
pack.MongoService = servicecfg.Database{
Name: fmt.Sprintf("%s-mongo", pack.name),
Protocol: defaults.ProtocolMongoDB,
URI: pack.mongoAddr,
}
cassandaListener, pack.cassandraAddr = mustListen(t)
pack.CassandraService = servicecfg.Database{
Name: fmt.Sprintf("%s-cassandra", pack.name),
Protocol: defaults.ProtocolCassandra,
URI: pack.cassandraAddr,
}
conf := servicecfg.MakeDefaultConfig()
conf.DataDir = filepath.Join(t.TempDir(), pack.name)
conf.SetToken("static-token-value")
conf.SetAuthServerAddress(utils.NetAddr{
AddrNetwork: "tcp",
Addr: pack.Cluster.Web,
})
conf.Databases.Enabled = true
conf.Databases.Databases = []servicecfg.Database{
pack.PostgresService,
pack.MysqlService,
pack.MongoService,
pack.CassandraService,
}
conf.Clock = clock
conf.CircuitBreakerConfig = breaker.NoopBreakerConfig()
pack.dbProcess, pack.dbAuthClient, err = pack.Cluster.StartDatabase(conf)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, pack.dbProcess.Close()) })
// Create and start test Postgres in the leaf cluster.
pack.postgres, err = postgres.NewTestServer(common.TestServerConfig{
AuthClient: pack.dbAuthClient,
Name: pack.PostgresService.Name,
Listener: postgresListener,
})
require.NoError(t, err)
go pack.postgres.Serve()
t.Cleanup(func() { pack.postgres.Close() })
// Create and start test MySQL in the leaf cluster.
pack.mysql, err = mysql.NewTestServer(common.TestServerConfig{
AuthClient: pack.dbAuthClient,
Name: pack.MysqlService.Name,
Listener: mysqlListener,
})
require.NoError(t, err)
go pack.mysql.Serve()
t.Cleanup(func() { pack.mysql.Close() })
// Create and start test Mongo in the leaf cluster.
pack.mongo, err = mongodb.NewTestServer(common.TestServerConfig{
AuthClient: pack.dbAuthClient,
Name: pack.MongoService.Name,
Listener: mongoListener,
})
require.NoError(t, err)
go pack.mongo.Serve()
t.Cleanup(func() { pack.mongo.Close() })
pack.cassandra, err = cassandra.NewTestServer(common.TestServerConfig{
AuthClient: pack.dbAuthClient,
Name: pack.CassandraService.Name,
Listener: cassandaListener,
})
require.NoError(t, err)
go pack.cassandra.Serve()
t.Cleanup(func() { pack.cassandra.Close() })
helpers.WaitForDatabaseServers(t, pack.Cluster.Process.GetAuthServer(), conf.Databases.Databases)
}
type testOptions struct {
clock clockwork.Clock
listenerSetup helpers.InstanceListenerSetupFunc
rootConfig func(config *servicecfg.Config)
leafConfig func(config *servicecfg.Config)
nodeName string
}
type TestOptionFunc func(*testOptions)
func (o *testOptions) setDefaultIfNotSet() {
if o.clock == nil {
o.clock = clockwork.NewRealClock()
}
if o.listenerSetup == nil {
o.listenerSetup = helpers.StandardListenerSetup
}
if o.nodeName == "" {
o.nodeName = helpers.Host
}
}
func WithClock(clock clockwork.Clock) TestOptionFunc {
return func(o *testOptions) {
o.clock = clock
}
}
func WithNodeName(nodeName string) TestOptionFunc {
return func(o *testOptions) {
o.nodeName = nodeName
}
}
func WithListenerSetupDatabaseTest(fn helpers.InstanceListenerSetupFunc) TestOptionFunc {
return func(o *testOptions) {
o.listenerSetup = fn
}
}
func WithRootConfig(fn func(*servicecfg.Config)) TestOptionFunc {
return func(o *testOptions) {
o.rootConfig = fn
}
}
func WithLeafConfig(fn func(*servicecfg.Config)) TestOptionFunc {
return func(o *testOptions) {
o.leafConfig = fn
}
}
func SetupDatabaseTest(t *testing.T, options ...TestOptionFunc) *DatabasePack {
ctx := context.Background()
var opts testOptions
for _, opt := range options {
opt(&opts)
}
opts.setDefaultIfNotSet()
// Some global setup.
tracer := utils.NewTracer(utils.ThisFunction()).Start()
t.Cleanup(func() { tracer.Stop() })
lib.SetInsecureDevMode(true)
log := utils.NewLoggerForTests()
// Generate keypair.
privateKey, publicKey, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
p := &DatabasePack{
clock: opts.clock,
Root: databaseClusterPack{name: "root"},
Leaf: databaseClusterPack{name: "leaf"},
}
// Create root cluster.
rootCfg := helpers.InstanceConfig{
ClusterName: "root.example.com",
HostID: uuid.New().String(),
NodeName: opts.nodeName,
Priv: privateKey,
Pub: publicKey,
Log: log,
}
rootCfg.Listeners = opts.listenerSetup(t, &rootCfg.Fds)
p.Root.Cluster = helpers.NewInstance(t, rootCfg)
// Create leaf cluster.
leafCfg := helpers.InstanceConfig{
ClusterName: "leaf.example.com",
HostID: uuid.New().String(),
NodeName: opts.nodeName,
Priv: privateKey,
Pub: publicKey,
Log: log,
}
leafCfg.Listeners = opts.listenerSetup(t, &leafCfg.Fds)
p.Leaf.Cluster = helpers.NewInstance(t, leafCfg)
// Make root cluster config.
rcConf := servicecfg.MakeDefaultConfig()
rcConf.DataDir = t.TempDir()
rcConf.Auth.Enabled = true
rcConf.Auth.Preference.SetSecondFactor("off")
rcConf.Proxy.Enabled = true
rcConf.Proxy.DisableWebInterface = true
rcConf.Clock = p.clock
rcConf.CircuitBreakerConfig = breaker.NoopBreakerConfig()
if opts.rootConfig != nil {
opts.rootConfig(rcConf)
}
// Make leaf cluster config.
lcConf := servicecfg.MakeDefaultConfig()
lcConf.DataDir = t.TempDir()
lcConf.Auth.Enabled = true
lcConf.Auth.Preference.SetSecondFactor("off")
lcConf.Proxy.Enabled = true
lcConf.Proxy.DisableWebInterface = true
lcConf.Clock = p.clock
lcConf.CircuitBreakerConfig = breaker.NoopBreakerConfig()
if opts.leafConfig != nil {
opts.leafConfig(lcConf)
}
// Establish trust b/w root and leaf.
err = p.Root.Cluster.CreateEx(t, p.Leaf.Cluster.Secrets.AsSlice(), rcConf)
require.NoError(t, err)
err = p.Leaf.Cluster.CreateEx(t, p.Root.Cluster.Secrets.AsSlice(), lcConf)
require.NoError(t, err)
// Start both clusters.
err = p.Leaf.Cluster.Start()
require.NoError(t, err)
t.Cleanup(func() {
p.Leaf.Cluster.StopAll()
})
err = p.Root.Cluster.Start()
require.NoError(t, err)
t.Cleanup(func() {
p.Root.Cluster.StopAll()
})
// Setup users and roles on both clusters.
p.setupUsersAndRoles(t)
// Update root's certificate authority on leaf to configure role mapping.
ca, err := p.Leaf.Cluster.Process.GetAuthServer().GetCertAuthority(ctx, types.CertAuthID{
Type: types.UserCA,
DomainName: p.Root.Cluster.Secrets.SiteName,
}, false)
require.NoError(t, err)
ca.SetRoles(nil) // Reset roles, otherwise they will take precedence.
ca.SetRoleMap(types.RoleMap{
{Remote: p.Root.role.GetName(), Local: []string{p.Leaf.role.GetName()}},
})
err = p.Leaf.Cluster.Process.GetAuthServer().UpsertCertAuthority(ctx, ca)
require.NoError(t, err)
// Start database service and test servers in the clusters
p.StartDatabases(t)
return p
}
func (p *DatabasePack) setupUsersAndRoles(t *testing.T) {
var err error
p.Root.User, p.Root.role, err = auth.CreateUserAndRole(p.Root.Cluster.Process.GetAuthServer(), "root-user", nil, nil)
require.NoError(t, err)
p.Root.role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
p.Root.role.SetDatabaseNames(types.Allow, []string{types.Wildcard})
err = p.Root.Cluster.Process.GetAuthServer().UpsertRole(context.Background(), p.Root.role)
require.NoError(t, err)
p.Leaf.User, p.Leaf.role, err = auth.CreateUserAndRole(p.Root.Cluster.Process.GetAuthServer(), "leaf-user", nil, nil)
require.NoError(t, err)
p.Leaf.role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
p.Leaf.role.SetDatabaseNames(types.Allow, []string{types.Wildcard})
err = p.Leaf.Cluster.Process.GetAuthServer().UpsertRole(context.Background(), p.Leaf.role)
require.NoError(t, err)
}
func (p *DatabasePack) WaitForLeaf(t *testing.T) {
helpers.WaitForProxyCount(p.Leaf.Cluster, p.Root.Cluster.Secrets.SiteName, 1)
site, err := p.Root.Cluster.Tunnel.GetSite(p.Leaf.Cluster.Secrets.SiteName)
require.NoError(t, err)
accessPoint, err := site.CachingAccessPoint()
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
servers, err := accessPoint.GetDatabaseServers(ctx, apidefaults.Namespace)
if err != nil {
// Use root logger as we need a configured logger instance and the root cluster have one.
p.Root.Cluster.Log.WithError(err).Debugf("Leaf cluster access point is unavailable.")
continue
}
if !containsDB(servers, p.Leaf.MysqlService.Name) {
p.Root.Cluster.Log.WithError(err).Debugf("Leaf db service %q is unavailable.", p.Leaf.MysqlService.Name)
continue
}
if !containsDB(servers, p.Leaf.PostgresService.Name) {
p.Root.Cluster.Log.WithError(err).Debugf("Leaf db service %q is unavailable.", p.Leaf.PostgresService.Name)
continue
}
return
case <-ctx.Done():
t.Fatal("Leaf cluster access point is unavailable.")
}
}
}
func (p *DatabasePack) StartDatabases(t *testing.T) {
p.Root.StartDatabaseServices(t, p.clock)
p.Leaf.StartDatabaseServices(t, p.clock)
}
// databaseAgentStartParams parameters used to configure a database agent.
type databaseAgentStartParams struct {
databases []servicecfg.Database
resourceMatchers []services.ResourceMatcher
}
// startRootDatabaseAgent starts a database agent with the provided
// configuration on the root cluster.
func (p *DatabasePack) startRootDatabaseAgent(t *testing.T, params databaseAgentStartParams) (*service.TeleportProcess, *auth.Client) {
conf := servicecfg.MakeDefaultConfig()
conf.DataDir = t.TempDir()
conf.SetToken("static-token-value")
conf.DiagnosticAddr = *utils.MustParseAddr(helpers.NewListener(t, service.ListenerDiagnostic, &conf.FileDescriptors))
conf.SetAuthServerAddress(utils.NetAddr{
AddrNetwork: "tcp",
Addr: p.Root.Cluster.Web,
})
conf.Clock = p.clock
conf.Databases.Enabled = true
conf.Databases.Databases = params.databases
conf.Databases.ResourceMatchers = params.resourceMatchers
conf.CircuitBreakerConfig = breaker.NoopBreakerConfig()
server, authClient, err := p.Root.Cluster.StartDatabase(conf)
require.NoError(t, err)
t.Cleanup(func() {
server.Close()
})
helpers.WaitForDatabaseServers(t, p.Root.Cluster.Process.GetAuthServer(), conf.Databases.Databases)
return server, authClient
}
func containsDB(servers []types.DatabaseServer, name string) bool {
for _, server := range servers {
if server.GetDatabase().GetName() == name {
return true
}
}
return false
}
// testLargeQuery tests a scenario where a user connects
// to a MySQL database running in a root cluster.
func (p *DatabasePack) testLargeQuery(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",
},
})
require.NoError(t, err)
now := time.Now()
query := fmt.Sprintf("select %s", strings.Repeat("A", 100*1024))
result, err := client.Execute(query)
require.NoError(t, err)
require.Equal(t, mysql.TestQueryResponse, result)
result.Close()
require.NoError(t, err)
require.Equal(t, mysql.TestQueryResponse, result)
result.Close()
ee := helpers.WaitForAuditEventTypeWithBackoff(t, p.Root.Cluster.Process.GetAuthServer(), now, events.DatabaseSessionQueryEvent)
require.Len(t, ee, 1)
query = "select 1"
result, err = client.Execute(query)
require.NoError(t, err)
require.Equal(t, mysql.TestQueryResponse, result)
result.Close()
require.Eventually(t, func() bool {
ee := helpers.WaitForAuditEventTypeWithBackoff(t, p.Root.Cluster.Process.GetAuthServer(), now, events.DatabaseSessionQueryEvent)
return len(ee) == 2
}, time.Second*3, time.Millisecond*500)
// Disconnect.
err = client.Close()
require.NoError(t, err)
}