mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 01:58:44 +08:00
* Add a new db engine * Add tests for new engine * Update tsh db subcommands * Refactor error message and suggestions for unsupported tsh commands * Add dynamodb to test plan * Add AWS external ID to db config and update protos
524 lines
15 KiB
Go
524 lines
15 KiB
Go
/*
|
|
Copyright 2015-2017 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 main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport/api/breaker"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apidefaults "github.com/gravitational/teleport/api/defaults"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/service"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// TestDatabaseLogin tests "tsh db login" command and verifies "tsh db
|
|
// env/config" after login.
|
|
func TestDatabaseLogin(t *testing.T) {
|
|
tmpHomePath := t.TempDir()
|
|
|
|
connector := mockConnector(t)
|
|
|
|
alice, err := types.NewUser("alice@example.com")
|
|
require.NoError(t, err)
|
|
alice.SetRoles([]string{"access"})
|
|
|
|
authProcess, proxyProcess := makeTestServers(t, withBootstrap(connector, alice))
|
|
makeTestDatabaseServer(t, authProcess, proxyProcess, service.Database{
|
|
Name: "postgres",
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:5432",
|
|
}, service.Database{
|
|
Name: "mongo",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
URI: "localhost:27017",
|
|
}, service.Database{
|
|
Name: "mssql",
|
|
Protocol: defaults.ProtocolSQLServer,
|
|
URI: "localhost:1433",
|
|
}, service.Database{
|
|
Name: "dynamodb",
|
|
Protocol: defaults.ProtocolDynamoDB,
|
|
URI: "", // uri can be blank for DynamoDB, it will be derived from the region and requests.
|
|
AWS: service.DatabaseAWS{
|
|
AccountID: "12345",
|
|
ExternalID: "123123123",
|
|
Region: "us-west-1",
|
|
},
|
|
})
|
|
|
|
authServer := authProcess.GetAuthServer()
|
|
require.NotNil(t, authServer)
|
|
|
|
proxyAddr, err := proxyProcess.ProxyWebAddr()
|
|
require.NoError(t, err)
|
|
|
|
// Log into Teleport cluster.
|
|
err = Run(context.Background(), []string{
|
|
"login", "--insecure", "--debug", "--auth", connector.GetName(), "--proxy", proxyAddr.String(),
|
|
}, setHomePath(tmpHomePath), cliOption(func(cf *CLIConf) error {
|
|
cf.mockSSOLogin = mockSSOLogin(t, authServer, alice)
|
|
return nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
databaseName string
|
|
expectCertsLen int
|
|
expectKeysLen int
|
|
expectErrForConfigCmd bool
|
|
expectErrForEnvCmd bool
|
|
}{
|
|
{
|
|
databaseName: "postgres",
|
|
expectCertsLen: 1,
|
|
},
|
|
{
|
|
databaseName: "mongo",
|
|
expectCertsLen: 1,
|
|
expectKeysLen: 1,
|
|
expectErrForEnvCmd: true, // "tsh db env" not supported for Mongo.
|
|
},
|
|
{
|
|
databaseName: "mssql",
|
|
expectCertsLen: 1,
|
|
expectErrForConfigCmd: true, // "tsh db config" not supported for MSSQL.
|
|
expectErrForEnvCmd: true, // "tsh db env" not supported for MSSQL.
|
|
},
|
|
{
|
|
databaseName: "dynamodb",
|
|
expectCertsLen: 1,
|
|
expectErrForConfigCmd: true, // "tsh db config" not supported for DynamoDB.
|
|
expectErrForEnvCmd: true, // "tsh db env" not supported for DynamoDB.
|
|
},
|
|
}
|
|
|
|
// Note: keystore currently races when multiple tsh clients work in the
|
|
// same profile dir (e.g. StatusCurrent might fail reading if someone else
|
|
// is writing a key at the same time). Thus running all `tsh db login` in
|
|
// sequence first before running other test cases in parallel.
|
|
for _, test := range testCases {
|
|
t.Run(fmt.Sprintf("%v/%v", "tsh db login", test.databaseName), func(t *testing.T) {
|
|
err := Run(context.Background(), []string{
|
|
"db", "login", "--db-user", "admin", test.databaseName,
|
|
}, setHomePath(tmpHomePath))
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the active profile.
|
|
profile, err := client.StatusFor(tmpHomePath, proxyAddr.Host(), alice.GetName())
|
|
require.NoError(t, err)
|
|
|
|
// Verify certificates.
|
|
certs, keys, err := decodePEM(profile.DatabaseCertPathForCluster("", test.databaseName))
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.expectCertsLen, len(certs)) // don't use require.Len, because it spams PEM bytes on fail.
|
|
require.Equal(t, test.expectKeysLen, len(keys)) // don't use require.Len, because it spams PEM bytes on fail.
|
|
})
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
test := test
|
|
|
|
t.Run(fmt.Sprintf("%v/%v", "tsh db config", test.databaseName), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := Run(context.Background(), []string{
|
|
"db", "config", test.databaseName,
|
|
}, setHomePath(tmpHomePath))
|
|
|
|
if test.expectErrForConfigCmd {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
t.Run(fmt.Sprintf("%v/%v", "tsh db env", test.databaseName), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := Run(context.Background(), []string{
|
|
"db", "env", test.databaseName,
|
|
}, setHomePath(tmpHomePath))
|
|
|
|
if test.expectErrForEnvCmd {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListDatabase(t *testing.T) {
|
|
lib.SetInsecureDevMode(true)
|
|
defer lib.SetInsecureDevMode(false)
|
|
|
|
s := newTestSuite(t,
|
|
withRootConfigFunc(func(cfg *service.Config) {
|
|
cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
|
|
cfg.Databases.Enabled = true
|
|
cfg.Databases.Databases = []service.Database{{
|
|
Name: "root-postgres",
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:5432",
|
|
}}
|
|
}),
|
|
withLeafCluster(),
|
|
withLeafConfigFunc(func(cfg *service.Config) {
|
|
cfg.Databases.Enabled = true
|
|
cfg.Databases.Databases = []service.Database{{
|
|
Name: "leaf-postgres",
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:5432",
|
|
}}
|
|
}),
|
|
)
|
|
|
|
mustLoginSetEnv(t, s)
|
|
|
|
captureStdout := new(bytes.Buffer)
|
|
err := Run(context.Background(), []string{
|
|
"db",
|
|
"ls",
|
|
"--insecure",
|
|
"--debug",
|
|
}, func(cf *CLIConf) error {
|
|
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Contains(t, captureStdout.String(), "root-postgres")
|
|
|
|
captureStdout.Reset()
|
|
err = Run(context.Background(), []string{
|
|
"db",
|
|
"ls",
|
|
"--cluster",
|
|
"leaf1",
|
|
"--insecure",
|
|
"--debug",
|
|
}, func(cf *CLIConf) error {
|
|
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Contains(t, captureStdout.String(), "leaf-postgres")
|
|
}
|
|
|
|
func TestFormatDatabaseListCommand(t *testing.T) {
|
|
t.Run("default", func(t *testing.T) {
|
|
require.Equal(t, "tsh db ls", formatDatabaseListCommand(""))
|
|
})
|
|
|
|
t.Run("with cluster flag", func(t *testing.T) {
|
|
require.Equal(t, "tsh db ls --cluster=leaf", formatDatabaseListCommand("leaf"))
|
|
})
|
|
}
|
|
|
|
func TestFormatConfigCommand(t *testing.T) {
|
|
db := tlsca.RouteToDatabase{
|
|
ServiceName: "example-db",
|
|
}
|
|
|
|
t.Run("default", func(t *testing.T) {
|
|
require.Equal(t, "tsh db config --format=cmd example-db", formatDatabaseConfigCommand("", db))
|
|
})
|
|
|
|
t.Run("with cluster flag", func(t *testing.T) {
|
|
require.Equal(t, "tsh db config --cluster=leaf --format=cmd example-db", formatDatabaseConfigCommand("leaf", db))
|
|
})
|
|
}
|
|
|
|
func TestDBInfoHasChanged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
databaseUserName string
|
|
databaseName string
|
|
db tlsca.RouteToDatabase
|
|
wantUserHasChanged bool
|
|
}{
|
|
{
|
|
name: "empty cli database user flag",
|
|
databaseUserName: "",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "alice",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: false,
|
|
},
|
|
{
|
|
name: "different user",
|
|
databaseUserName: "alice",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "bob",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: true,
|
|
},
|
|
{
|
|
name: "different user mysql protocol",
|
|
databaseUserName: "alice",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "bob",
|
|
Protocol: defaults.ProtocolMySQL,
|
|
},
|
|
wantUserHasChanged: true,
|
|
},
|
|
{
|
|
name: "same user",
|
|
databaseUserName: "bob",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "bob",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: false,
|
|
},
|
|
{
|
|
name: "empty cli database user and database name flags",
|
|
databaseUserName: "",
|
|
databaseName: "",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "alice",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: false,
|
|
},
|
|
{
|
|
name: "different database name",
|
|
databaseUserName: "",
|
|
databaseName: "db1",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "alice",
|
|
Database: "db2",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: true,
|
|
},
|
|
{
|
|
name: "same database name",
|
|
databaseUserName: "",
|
|
databaseName: "db1",
|
|
db: tlsca.RouteToDatabase{
|
|
Username: "alice",
|
|
Database: "db1",
|
|
Protocol: defaults.ProtocolMongoDB,
|
|
},
|
|
wantUserHasChanged: false,
|
|
},
|
|
}
|
|
|
|
ca, err := tlsca.FromKeys([]byte(fixtures.TLSCACertPEM), []byte(fixtures.TLSCAKeyPEM))
|
|
require.NoError(t, err)
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, constants.RSAKeySize)
|
|
require.NoError(t, err)
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
identity := tlsca.Identity{
|
|
Username: "user",
|
|
RouteToDatabase: tc.db,
|
|
Groups: []string{"none"},
|
|
}
|
|
subj, err := identity.Subject()
|
|
require.NoError(t, err)
|
|
certBytes, err := ca.GenerateCertificate(tlsca.CertificateRequest{
|
|
PublicKey: privateKey.Public(),
|
|
Subject: subj,
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
certPath := filepath.Join(t.TempDir(), "mongo_db_cert.pem")
|
|
require.NoError(t, os.WriteFile(certPath, certBytes, 0o600))
|
|
|
|
cliConf := &CLIConf{DatabaseUser: tc.databaseUserName, DatabaseName: tc.databaseName}
|
|
got, err := dbInfoHasChanged(cliConf, certPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.wantUserHasChanged, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeTestDatabaseServer(t *testing.T, auth *service.TeleportProcess, proxy *service.TeleportProcess, dbs ...service.Database) (db *service.TeleportProcess) {
|
|
// Proxy uses self-signed certificates in tests.
|
|
lib.SetInsecureDevMode(true)
|
|
|
|
cfg := service.MakeDefaultConfig()
|
|
cfg.Hostname = "localhost"
|
|
cfg.DataDir = t.TempDir()
|
|
cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
|
|
|
|
proxyAddr, err := proxy.ProxyWebAddr()
|
|
require.NoError(t, err)
|
|
|
|
cfg.SetAuthServerAddress(*proxyAddr)
|
|
|
|
token, err := proxy.Config.Token()
|
|
require.NoError(t, err)
|
|
|
|
cfg.SetToken(token)
|
|
cfg.SSH.Enabled = false
|
|
cfg.Auth.Enabled = false
|
|
cfg.Proxy.Enabled = false
|
|
cfg.Databases.Enabled = true
|
|
cfg.Databases.Databases = dbs
|
|
cfg.Log = utils.NewLoggerForTests()
|
|
|
|
db = runTeleport(t, cfg)
|
|
|
|
// Wait for all databases to register to avoid races.
|
|
waitForDatabases(t, auth, dbs)
|
|
return db
|
|
}
|
|
|
|
func waitForDatabases(t *testing.T, auth *service.TeleportProcess, dbs []service.Database) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
for {
|
|
select {
|
|
case <-time.After(500 * time.Millisecond):
|
|
all, err := auth.GetAuthServer().GetDatabaseServers(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// Count how many input "dbs" are registered.
|
|
var registered int
|
|
for _, db := range dbs {
|
|
for _, a := range all {
|
|
if a.GetName() == db.Name {
|
|
registered++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if registered == len(dbs) {
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("databases not registered after 10s")
|
|
}
|
|
}
|
|
}
|
|
|
|
// decodePEM sorts out specified PEM file into certificates and private keys.
|
|
func decodePEM(pemPath string) (certs []pem.Block, privs []pem.Block, err error) {
|
|
bytes, err := os.ReadFile(pemPath)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
var block *pem.Block
|
|
for {
|
|
block, bytes = pem.Decode(bytes)
|
|
if block == nil {
|
|
break
|
|
}
|
|
switch block.Type {
|
|
case "CERTIFICATE":
|
|
certs = append(certs, *block)
|
|
case keys.PKCS1PrivateKeyType:
|
|
privs = append(privs, *block)
|
|
case keys.PKCS8PrivateKeyType:
|
|
privs = append(privs, *block)
|
|
}
|
|
}
|
|
return certs, privs, nil
|
|
}
|
|
|
|
func TestFormatDatabaseConnectArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cluster string
|
|
route tlsca.RouteToDatabase
|
|
wantFlags []string
|
|
}{
|
|
{
|
|
name: "match user and db name, cluster set",
|
|
cluster: "foo",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMongoDB, ServiceName: "svc"},
|
|
wantFlags: []string{"--cluster=foo", "--db-user=<user>", "--db-name=<name>", "svc"},
|
|
},
|
|
{
|
|
name: "match user and db name",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMongoDB, ServiceName: "svc"},
|
|
wantFlags: []string{"--db-user=<user>", "--db-name=<name>", "svc"},
|
|
},
|
|
{
|
|
name: "match user and db name, username given",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMongoDB, Username: "bob", ServiceName: "svc"},
|
|
wantFlags: []string{"--db-name=<name>", "svc"},
|
|
},
|
|
{
|
|
name: "match user and db name, db name given",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMongoDB, Database: "sales", ServiceName: "svc"},
|
|
wantFlags: []string{"--db-user=<user>", "svc"},
|
|
},
|
|
{
|
|
name: "match user and db name, both given",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMongoDB, Database: "sales", Username: "bob", ServiceName: "svc"},
|
|
wantFlags: []string{"svc"},
|
|
},
|
|
{
|
|
name: "match user name",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMySQL, ServiceName: "svc"},
|
|
wantFlags: []string{"--db-user=<user>", "svc"},
|
|
},
|
|
{
|
|
name: "match user name, given",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolMySQL, Username: "bob", ServiceName: "svc"},
|
|
wantFlags: []string{"svc"},
|
|
},
|
|
{
|
|
name: "match user name, dynamodb",
|
|
cluster: "",
|
|
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolDynamoDB, ServiceName: "svc"},
|
|
wantFlags: []string{"--db-user=<user>", "svc"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
out := formatDatabaseConnectArgs(tt.cluster, tt.route)
|
|
require.Equal(t, tt.wantFlags, out)
|
|
})
|
|
}
|
|
}
|