Skip to content

Commit

Permalink
Add MongoDB database access support (gravitational#7213)
Browse files Browse the repository at this point in the history
  • Loading branch information
r0mant authored Jun 22, 2021
1 parent fa010b2 commit 6b9726f
Show file tree
Hide file tree
Showing 458 changed files with 85,906 additions and 442 deletions.
601 changes: 324 additions & 277 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions api/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,9 @@ message DatabaseSessionQuery {
// DatabaseQueryParameters are the query parameters for prepared statements.
repeated string DatabaseQueryParameters = 6
[ (gogoproto.jsontag) = "db_query_parameters,omitempty" ];
// Status indicates whether the query was successfully sent to the database.
Status Status = 7
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
}

// DatabaseSessionEnd is emitted when a user ends the database session.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/google/go-cmp v0.5.4
github.com/google/gops v0.3.14
github.com/google/uuid v1.2.0
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
Expand Down Expand Up @@ -91,6 +92,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c // indirect
github.com/xeipuuv/gojsonschema v0.0.0-20151204154511-3988ac14d6f6 // indirect
go.etcd.io/etcd v0.5.0-alpha.5.0.20201125193152-8a03d2e9614b
go.mongodb.org/mongo-driver v1.5.3
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.7.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
Expand Down
68 changes: 66 additions & 2 deletions go.sum

Large diffs are not rendered by default.

114 changes: 111 additions & 3 deletions integration/db_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 Gravitational, Inc.
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.
Expand Down Expand Up @@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/services"
"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"
Expand All @@ -45,6 +46,7 @@ import (
"github.com/siddontang/go-mysql/client"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
)

// TestDatabaseAccessPostgresRootCluster tests a scenario where a user connects
Expand Down Expand Up @@ -181,6 +183,65 @@ func TestDatabaseAccessMySQLLeafCluster(t *testing.T) {
require.NoError(t, err)
}

// TestDatabaseAccessMongoRootCluster tests a scenario where a user connects
// to a Mongo database running in a root cluster.
func TestDatabaseAccessMongoRootCluster(t *testing.T) {
pack := setupDatabaseTest(t)

// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: pack.root.cluster.GetSiteAPI(pack.root.cluster.Secrets.SiteName),
AuthServer: pack.root.cluster.Process.GetAuthServer(),
Address: net.JoinHostPort(Loopback, pack.root.cluster.GetPortWeb()),
Cluster: pack.root.cluster.Secrets.SiteName,
Username: pack.root.user.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: pack.root.mongoService.Name,
Protocol: pack.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)
}

// TestDatabaseAccessMongoLeafCluster tests a scenario where a user connects
// to a Mongo database running in a leaf cluster.
func TestDatabaseAccessMongoLeafCluster(t *testing.T) {
pack := setupDatabaseTest(t)
pack.waitForLeaf(t)

// Connect to the database service in root cluster.
client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{
AuthClient: pack.root.cluster.GetSiteAPI(pack.root.cluster.Secrets.SiteName),
AuthServer: pack.root.cluster.Process.GetAuthServer(),
Address: net.JoinHostPort(Loopback, pack.root.cluster.GetPortWeb()), // Connecting via root cluster.
Cluster: pack.leaf.cluster.Secrets.SiteName,
Username: pack.root.user.GetName(),
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: pack.leaf.mongoService.Name,
Protocol: pack.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) {
Expand Down Expand Up @@ -311,6 +372,9 @@ type databaseClusterPack struct {
mysqlService service.Database
mysqlAddr string
mysql *mysql.TestServer
mongoService service.Database
mongoAddr string
mongo *mongodb.TestServer
}

type testOptions struct {
Expand Down Expand Up @@ -354,10 +418,12 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
root: databaseClusterPack{
postgresAddr: net.JoinHostPort("localhost", ports.Pop()),
mysqlAddr: net.JoinHostPort("localhost", ports.Pop()),
mongoAddr: net.JoinHostPort("localhost", ports.Pop()),
},
leaf: databaseClusterPack{
postgresAddr: net.JoinHostPort("localhost", ports.Pop()),
mysqlAddr: net.JoinHostPort("localhost", ports.Pop()),
mongoAddr: net.JoinHostPort("localhost", ports.Pop()),
},
}

Expand Down Expand Up @@ -446,6 +512,11 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
Protocol: defaults.ProtocolMySQL,
URI: p.root.mysqlAddr,
}
p.root.mongoService = service.Database{
Name: "root-mongo",
Protocol: defaults.ProtocolMongoDB,
URI: p.root.mongoAddr,
}
rdConf := service.MakeDefaultConfig()
rdConf.DataDir = t.TempDir()
rdConf.Token = "static-token-value"
Expand All @@ -456,7 +527,11 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
},
}
rdConf.Databases.Enabled = true
rdConf.Databases.Databases = []service.Database{p.root.postgresService, p.root.mysqlService}
rdConf.Databases.Databases = []service.Database{
p.root.postgresService,
p.root.mysqlService,
p.root.mongoService,
}
rdConf.Clock = p.clock
p.root.dbProcess, p.root.dbAuthClient, err = p.root.cluster.StartDatabase(rdConf)
require.NoError(t, err)
Expand All @@ -475,6 +550,11 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
Protocol: defaults.ProtocolMySQL,
URI: p.leaf.mysqlAddr,
}
p.leaf.mongoService = service.Database{
Name: "leaf-mongo",
Protocol: defaults.ProtocolMongoDB,
URI: p.leaf.mongoAddr,
}
ldConf := service.MakeDefaultConfig()
ldConf.DataDir = t.TempDir()
ldConf.Token = "static-token-value"
Expand All @@ -485,7 +565,11 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
},
}
ldConf.Databases.Enabled = true
ldConf.Databases.Databases = []service.Database{p.leaf.postgresService, p.leaf.mysqlService}
ldConf.Databases.Databases = []service.Database{
p.leaf.postgresService,
p.leaf.mysqlService,
p.leaf.mongoService,
}
ldConf.Clock = p.clock
p.leaf.dbProcess, p.leaf.dbAuthClient, err = p.leaf.cluster.StartDatabase(ldConf)
require.NoError(t, err)
Expand Down Expand Up @@ -517,6 +601,18 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
p.root.mysql.Close()
})

// Create and start test Mongo in the root cluster.
p.root.mongo, err = mongodb.NewTestServer(common.TestServerConfig{
AuthClient: p.root.dbAuthClient,
Name: p.root.mongoService.Name,
Address: p.root.mongoAddr,
})
require.NoError(t, err)
go p.root.mongo.Serve()
t.Cleanup(func() {
p.root.mongo.Close()
})

// Create and start test Postgres in the leaf cluster.
p.leaf.postgres, err = postgres.NewTestServer(common.TestServerConfig{
AuthClient: p.leaf.dbAuthClient,
Expand All @@ -541,6 +637,18 @@ func setupDatabaseTest(t *testing.T, options ...testOptionFunc) *databasePack {
p.leaf.mysql.Close()
})

// Create and start test Mongo in the leaf cluster.
p.leaf.mongo, err = mongodb.NewTestServer(common.TestServerConfig{
AuthClient: p.leaf.dbAuthClient,
Name: p.leaf.mongoService.Name,
Address: p.leaf.mongoAddr,
})
require.NoError(t, err)
go p.leaf.mongo.Serve()
t.Cleanup(func() {
p.leaf.mongo.Close()
})

return p
}

Expand Down
18 changes: 18 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,19 @@ type certRequest struct {
clientIP string
}

// check verifies the cert request is valid.
func (r *certRequest) check() error {
// When generating certificate for MongoDB access, database username must
// be encoded into it. This is required to be able to tell which database
// user to authenticate the connection as.
if r.dbProtocol == defaults.ProtocolMongoDB {
if r.dbUser == "" {
return trace.BadParameter("must provide database user name to generate certificate for database %q", r.dbService)
}
}
return nil
}

type certRequestOption func(*certRequest)

func certRequestMFAVerified(mfaID string) certRequestOption {
Expand Down Expand Up @@ -714,6 +727,11 @@ func (a *Server) GenerateDatabaseTestCert(req DatabaseTestCertRequest) ([]byte,

// generateUserCert generates user certificates
func (a *Server) generateUserCert(req certRequest) (*certs, error) {
err := req.check()
if err != nil {
return nil, trace.Wrap(err)
}

// reuse the same RSA keys for SSH and TLS keys
cryptoPubKey, err := sshutils.CryptoPublicKey(req.publicKey)
if err != nil {
Expand Down
17 changes: 15 additions & 2 deletions lib/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/scp"
Expand Down Expand Up @@ -284,15 +285,27 @@ func (proxy *ProxyClient) reissueUserCerts(ctx context.Context, cachePolicy Cert
// Database certs have to be requested with CertUsage All because
// pre-7.0 servers do not accept usage-restricted certificates.
if params.RouteToDatabase.ServiceName != "" {
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = certs.TLS
switch params.RouteToDatabase.Protocol {
case defaults.ProtocolMongoDB:
// MongoDB expects certificate and key pair in the same pem file.
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = append(certs.TLS, key.Priv...)
default:
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = certs.TLS
}
}

case proto.UserCertsRequest_SSH:
key.Cert = certs.SSH
case proto.UserCertsRequest_App:
key.AppTLSCerts[params.RouteToApp.Name] = certs.TLS
case proto.UserCertsRequest_Database:
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = certs.TLS
switch params.RouteToDatabase.Protocol {
case defaults.ProtocolMongoDB:
// MongoDB expects certificate and key pair in the same pem file.
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = append(certs.TLS, key.Priv...)
default:
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = certs.TLS
}
case proto.UserCertsRequest_Kubernetes:
key.KubeTLSCerts[params.KubernetesCluster] = certs.TLS
}
Expand Down
16 changes: 0 additions & 16 deletions lib/client/db/mysql/optionfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"path/filepath"
"strconv"
"strings"
"text/template"

"github.com/gravitational/teleport/lib/client/db/profile"

Expand Down Expand Up @@ -149,18 +148,3 @@ const (
// mysqlOptionFile is the default name of the MySQL option file.
mysqlOptionFile = ".my.cnf"
)

// Message is printed after MySQL option file has been updated.
var Message = template.Must(template.New("").Parse(`
Connection information for MySQL database "{{.Name}}" has been saved.
You can now connect to the database using the following command:
$ mysql --defaults-group-suffix=_{{.Name}}
Or configure environment variables and use regular CLI flags:
$ eval $(tsh db env)
$ mysql
`))
16 changes: 0 additions & 16 deletions lib/client/db/postgres/servicefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"path/filepath"
"strconv"
"strings"
"text/template"

"github.com/gravitational/teleport/lib/client/db/profile"

Expand Down Expand Up @@ -188,18 +187,3 @@ const (
// pgServiceFile is the default name of the Postgres service file.
pgServiceFile = ".pg_service.conf"
)

// Message is printed after Postgres service file has been updated.
var Message = template.Must(template.New("").Parse(`
Connection information for PostgreSQL database "{{.Name}}" has been saved.
You can now connect to the database using the following command:
$ psql "service={{.Name}}{{if not .User}} user=<user>{{end}}{{if not .Database}} dbname=<dbname>{{end}}"
Or configure environment variables and use regular CLI flags:
$ eval $(tsh db env)
$ psql{{if not .User}} -U <user>{{end}}{{if not .Database}} <dbname>{{end}}
`))
Loading

0 comments on commit 6b9726f

Please sign in to comment.