diff --git a/docs/config.json b/docs/config.json
index d1b89dfb69ec1..52f4ae742884b 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -189,7 +189,7 @@
{ "title": "GCP Cloud SQL MySQL", "slug": "/database-access/guides/mysql-cloudsql/" },
{ "title": "MongoDB Atlas", "slug": "/database-access/guides/mongodb-atlas/" },
{ "title": "Self-Hosted PostgreSQL", "slug": "/database-access/guides/postgres-self-hosted/" },
- { "title": "Self-Hosted MySQL", "slug": "/database-access/guides/mysql-self-hosted/" },
+ { "title": "Self-Hosted MySQL/MariaDB", "slug": "/database-access/guides/mysql-self-hosted/" },
{ "title": "Self-Hosted MongoDB", "slug": "/database-access/guides/mongodb-self-hosted/" },
{ "title": "Self-Hosted CockroachDB", "slug": "/database-access/guides/cockroachdb-self-hosted/" },
{ "title": "Database GUI Clients", "slug": "/database-access/guides/gui-clients/" },
diff --git a/docs/pages/database-access/faq.mdx b/docs/pages/database-access/faq.mdx
index bd4647b9e7261..728d95dd5ac96 100644
--- a/docs/pages/database-access/faq.mdx
+++ b/docs/pages/database-access/faq.mdx
@@ -7,7 +7,7 @@ description: Frequently asked questions about Teleport Database Access.
## Which database protocols does Teleport Database Access support?
-Teleport Database Access currently supports PostgreSQL, MySQL, and MongoDB
+Teleport Database Access currently supports PostgreSQL, MySQL, MariaDB and MongoDB
protocols.
For PostgreSQL and MySQL, both self-hosted and cloud-hosted versions such as AWS
diff --git a/docs/pages/database-access/guides.mdx b/docs/pages/database-access/guides.mdx
index ef83e4ea31ce3..65043274bac7f 100644
--- a/docs/pages/database-access/guides.mdx
+++ b/docs/pages/database-access/guides.mdx
@@ -32,8 +32,8 @@ layout: tocless-doc
Connect self-hosted PostgreSQL database.
-
- Connect self-hosted MySQL database.
+
+ Connect self-hosted MySQL/MariaDB database.
Connect self-hosted MongoDB database.
diff --git a/docs/pages/database-access/guides/mysql-self-hosted.mdx b/docs/pages/database-access/guides/mysql-self-hosted.mdx
index 046ea01b2580e..559d3418e5f73 100644
--- a/docs/pages/database-access/guides/mysql-self-hosted.mdx
+++ b/docs/pages/database-access/guides/mysql-self-hosted.mdx
@@ -1,9 +1,9 @@
---
-title: Database Access with Self-Hosted MySQL
-description: How to configure Teleport Database Access with self-hosted MySQL.
+title: Database Access with Self-Hosted MySQL/MariaDB
+description: How to configure Teleport Database Access with self-hosted MySQL/MariaDB.
---
-# Self-Hosted MySQL
+# Self-Hosted MySQL/MariaDB
## Create Certificate/Key Pair
@@ -22,9 +22,11 @@ $ tctl auth sign --format=db --host=db.example.com --out=server --ttl=2190h
The command will create 3 files: `server.cas`, `server.crt` and `server.key`
which you'll need to enable mutual TLS on your MySQL server.
-## Configure MySQL Server
+## Configure MySQL/MariaDB Server
-To configure MySQL server to accept TLS connections, add the following to
+
+
+ To configure MySQL server to accept TLS connections, add the following to
MySQL configuration file `mysql.cnf`:
```conf
@@ -34,8 +36,22 @@ ssl-ca=/path/to/server.cas
ssl-cert=/path/to/server.crt
ssl-key=/path/to/server.key
```
+
+
+ To configure MariaDB server to accept TLS connections, add the following to
+MariaDB configuration file `mysql.cnf`:
-Additionally, MySQL database user accounts must be configured to require a
+```conf
+[mariadb]
+require_secure_transport=ON
+ssl-ca=/path/to/server.cas
+ssl-cert=/path/to/server.crt
+ssl-key=/path/to/server.key
+```
+
+
+
+Additionally, MySQL/MariaDB database user accounts must be configured to require a
valid client certificate. If you're creating a new user:
```sql
@@ -56,11 +72,11 @@ GRANT ALL ON `%`.* TO 'alice'@'%';
```
See [Configuring MySQL to Use Encrypted Connections](https://dev.mysql.com/doc/refman/8.0/en/using-encrypted-connections.html)
-in MySQL documentation for more details.
+in MySQL or [Enabling TLS on MariaDB Server](https://mariadb.com/docs/security/encryption/in-transit/enable-tls-server/) in MariaDB documentation for more details.
## Setup Teleport Auth and Proxy Services
-Teleport Database Access for MySQL is available starting from `6.0` release.
+Teleport Database Access for MySQL is available starting from `6.0` and MariaDB starting from `8.0` release.
(!docs/pages/includes/database-access/start-auth-proxy.mdx!)
@@ -143,7 +159,7 @@ db_service:
description: "Example MySQL"
# Database protocol.
protocol: "mysql"
- # Database address, MySQL server endpoint in this case.
+ # Database address, MySQL/MariaDB server endpoint in this case.
#
# Note: this URI's hostname must match the host name specified via --host
# flag to tctl auth sign command.
@@ -218,8 +234,8 @@ $ tsh db connect example
```
- The `mysql` command-line client should be available in PATH in order to be
- able to connect.
+ The `mysql` or `mariadb` command-line client should be available in PATH in order to be
+ able to connect. `mariadb` is a default command-line client for MySQL and MariaDB.
To log out of the database and remove credentials:
diff --git a/docs/pages/database-access/guides/rds.mdx b/docs/pages/database-access/guides/rds.mdx
index b31ebd99c8c97..283906f1f8e86 100644
--- a/docs/pages/database-access/guides/rds.mdx
+++ b/docs/pages/database-access/guides/rds.mdx
@@ -374,7 +374,7 @@ $ tsh db connect postgres-rds
```
- The appropriate database command-line client (`psql`, `mysql`) should be
+ The appropriate database command-line client (`psql`, `mysql`, `mariadb`) should be
available in PATH in order to be able to connect.
diff --git a/docs/pages/database-access/introduction.mdx b/docs/pages/database-access/introduction.mdx
index 6da7780ee45bb..7b33638b954b0 100644
--- a/docs/pages/database-access/introduction.mdx
+++ b/docs/pages/database-access/introduction.mdx
@@ -5,7 +5,7 @@ description: Teleport Database Access introduction, demo and resources.
# Database Access
-Teleport can provide secure access to PostgreSQL, MySQL and MongoDB databases,
+Teleport can provide secure access to PostgreSQL, MySQL, MariaDB and MongoDB databases,
while improving both access control and visibility.
Some of the things you can do with Database Access:
@@ -75,8 +75,8 @@ with Github, execute a few SQL queries and observe them in the audit log:
Connect self-hosted PostgreSQL database.
-
- Connect self-hosted MySQL database.
+
+ Connect self-hosted MySQL/MariaDB database.
Connect self-hosted MongoDB database.
diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx
index 39ce4aac90ef3..63e6bf87ef3c3 100644
--- a/docs/pages/index.mdx
+++ b/docs/pages/index.mdx
@@ -25,7 +25,7 @@ Teleport is a Certificate Authority and an Access Plane for your infrastructure.
Single Sign-On, audit and unified access for Kubernetes clusters.
- Secure access to PostgreSQL, MySQL and MongoDB databases.
+ Secure access to PostgreSQL, MySQL, MariaDB and MongoDB databases.
Secure browser-based access to desktop environments.
diff --git a/lib/client/db/mysql/optionfile.go b/lib/client/db/mysql/optionfile.go
index bc5fb7f865165..f88da68986504 100644
--- a/lib/client/db/mysql/optionfile.go
+++ b/lib/client/db/mysql/optionfile.go
@@ -111,7 +111,7 @@ func (o *OptionFile) Env(name string) (map[string]string, error) {
// https://dev.mysql.com/doc/refman/8.0/en/environment-variables.html
//
// Due to this fact, we use the "option group suffix" which makes clients
- // use speficic section from ~/.my.cnf file that has all these settings.
+ // use specific section from ~/.my.cnf file that has all these settings.
return map[string]string{
"MYSQL_GROUP_SUFFIX": suffix(name),
}, nil
diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go
index 55469341af22a..596cf92cd32d2 100644
--- a/lib/defaults/defaults.go
+++ b/lib/defaults/defaults.go
@@ -493,13 +493,13 @@ const (
const (
// ProtocolPostgres is the PostgreSQL database protocol.
ProtocolPostgres = "postgres"
- // ProtocolMySQL is the MySQL database protocol.
+ // ProtocolMySQL is the MySQL/MariaDB database protocol.
ProtocolMySQL = "mysql"
// ProtocolMongoDB is the MongoDB database protocol.
ProtocolMongoDB = "mongodb"
// ProtocolCockroachDB is the CockroachDB database protocol.
//
- // Technically it's the same as the Postgres protocol but it's used to
+ // Technically it's the same as the Postgres protocol, but it's used to
// differentiate between Cockroach and Postgres databases e.g. when
// selecting a CLI client to use.
ProtocolCockroachDB = "cockroachdb"
diff --git a/tool/tsh/db.go b/tool/tsh/db.go
index b8d0e898d5784..af660d978d733 100644
--- a/tool/tsh/db.go
+++ b/tool/tsh/db.go
@@ -21,16 +21,13 @@ import (
"io/ioutil"
"net"
"os"
- "os/exec"
"sort"
- "strconv"
"strings"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
dbprofile "github.com/gravitational/teleport/lib/client/db"
- "github.com/gravitational/teleport/lib/client/db/postgres"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/tlsca"
@@ -137,7 +134,7 @@ func databaseLogin(cf *CLIConf, tc *client.TeleportClient, db tlsca.RouteToDatab
}
// Print after-connect message.
if !quiet {
- fmt.Println(formatDatabaseConnnectMessage(cf.SiteName, db))
+ fmt.Println(formatDatabaseConnectMessage(cf.SiteName, db))
return nil
}
return nil
@@ -250,7 +247,7 @@ func onDatabaseConfig(cf *CLIConf) error {
}
switch cf.Format {
case dbFormatCommand:
- cmd, err := getConnectCommand(cf, tc, profile, database)
+ cmd, err := newCmdBuilder(tc, profile, database).getConnectCommand()
if err != nil {
return trace.Wrap(err)
}
@@ -332,7 +329,7 @@ func onDatabaseConnect(cf *CLIConf) error {
host := "localhost"
opts = append(opts, WithLocalProxy(host, addr.Port(0), profile.CACertPath()))
}
- cmd, err := getConnectCommand(cf, tc, profile, database, opts...)
+ cmd, err := newCmdBuilder(tc, profile, database, opts...).getConnectCommand()
if err != nil {
return trace.Wrap(err)
}
@@ -488,7 +485,7 @@ func isMFADatabaseAccessRequired(cf *CLIConf, tc *client.TeleportClient, databas
// pickActiveDatabase returns the database the current profile is logged into.
//
// If logged into multiple databases, returns an error unless one specified
-// explicily via --db flag.
+// explicitly via --db flag.
func pickActiveDatabase(cf *CLIConf) (*tlsca.RouteToDatabase, error) {
profile, err := client.StatusCurrent(cf.HomePath, cf.Proxy)
if err != nil {
@@ -547,94 +544,6 @@ func WithLocalProxy(host string, port int, caPath string) ConnectCommandFunc {
}
}
-func getConnectCommand(cf *CLIConf, tc *client.TeleportClient, profile *client.ProfileStatus, db *tlsca.RouteToDatabase, opts ...ConnectCommandFunc) (*exec.Cmd, error) {
- var options connectionCommandOpts
- for _, opt := range opts {
- opt(&options)
- }
-
- // In TLS routing mode a local proxy is started on demand so connect to it.
- host, port := tc.DatabaseProxyHostPort(*db)
- if options.localProxyPort != 0 && options.localProxyHost != "" {
- host = options.localProxyHost
- port = options.localProxyPort
- }
-
- switch db.Protocol {
- case defaults.ProtocolPostgres:
- return getPostgresCommand(tc, profile, db, host, port, options), nil
-
- case defaults.ProtocolCockroachDB:
- return getCockroachCommand(tc, profile, db, host, port, options), nil
-
- case defaults.ProtocolMySQL:
- return getMySQLCommand(tc, profile, db, options), nil
-
- case defaults.ProtocolMongoDB:
- return getMongoCommand(tc, profile, db, host, port, options), nil
- }
-
- return nil, trace.BadParameter("unsupported database protocol: %v", db)
-}
-
-func getPostgresCommand(tc *client.TeleportClient, profile *client.ProfileStatus, db *tlsca.RouteToDatabase, host string, port int, options connectionCommandOpts) *exec.Cmd {
- return exec.Command(postgresBin,
- postgres.GetConnString(dbprofile.New(tc, *db, *profile, host, port)))
-}
-
-func getCockroachCommand(tc *client.TeleportClient, profile *client.ProfileStatus, db *tlsca.RouteToDatabase, host string, port int, options connectionCommandOpts) *exec.Cmd {
- // If cockroach CLI client is not available, fallback to psql.
- if _, err := exec.LookPath(cockroachBin); err != nil {
- log.Debugf("Couldn't find %q client in PATH, falling back to %q: %v.",
- cockroachBin, postgresBin, err)
- return exec.Command(postgresBin,
- postgres.GetConnString(dbprofile.New(tc, *db, *profile, host, port)))
- }
- return exec.Command(cockroachBin, "sql", "--url",
- postgres.GetConnString(dbprofile.New(tc, *db, *profile, host, port)))
-}
-
-func getMySQLCommand(tc *client.TeleportClient, profile *client.ProfileStatus, db *tlsca.RouteToDatabase, options connectionCommandOpts) *exec.Cmd {
- args := []string{fmt.Sprintf("--defaults-group-suffix=_%v-%v", tc.SiteName, db.ServiceName)}
- if db.Username != "" {
- args = append(args, "--user", db.Username)
- }
- if db.Database != "" {
- args = append(args, "--database", db.Database)
- }
-
- if options.localProxyPort != 0 {
- args = append(args, "--port", strconv.Itoa(options.localProxyPort))
- args = append(args, "--host", options.localProxyHost)
- // MySQL CLI treats localhost as a special value and tries to use Unix Domain Socket for connection
- // To enforce TCP connection protocol needs to be explicitly specified.
- if options.localProxyHost == "localhost" {
- args = append(args, "--protocol", "TCP")
- }
- }
-
- return exec.Command(mysqlBin, args...)
-}
-
-func getMongoCommand(tc *client.TeleportClient, profile *client.ProfileStatus, db *tlsca.RouteToDatabase, host string, port int, options connectionCommandOpts) *exec.Cmd {
- args := []string{
- "--host", host,
- "--port", strconv.Itoa(port),
- "--ssl",
- "--sslPEMKeyFile", profile.DatabaseCertPathForCluster(tc.SiteName, db.ServiceName),
- }
-
- if options.caPath != "" {
- // caPath is set only if mongo connects to the Teleport Proxy via ALPN SNI Local Proxy
- // and connection is terminated by proxy identity certificate.
- args = append(args, []string{"--sslCAFile", options.caPath}...)
- }
- if db.Database != "" {
- args = append(args, db.Database)
- }
- return exec.Command(mongoBin, args...)
-}
-
func formatDatabaseListCommand(clusterFlag string) string {
if clusterFlag == "" {
return "tsh db ls"
@@ -649,7 +558,7 @@ func formatDatabaseConfigCommand(clusterFlag string, db tlsca.RouteToDatabase) s
return fmt.Sprintf("tsh db config --cluster=%v --format=cmd %v", clusterFlag, db.ServiceName)
}
-func formatDatabaseConnnectMessage(clusterFlag string, db tlsca.RouteToDatabase) string {
+func formatDatabaseConnectMessage(clusterFlag string, db tlsca.RouteToDatabase) string {
connectCommand := formatConnectCommand(clusterFlag, db)
configCommand := formatDatabaseConfigCommand(clusterFlag, db)
@@ -676,14 +585,3 @@ const (
// dbFormatCommand prints database connection command.
dbFormatCommand = "cmd"
)
-
-const (
- // postgresBin is the Postgres client binary name.
- postgresBin = "psql"
- // cockroachBin is the Cockroach client binary name.
- cockroachBin = "cockroach"
- // mysqlBin is the MySQL client binary name.
- mysqlBin = "mysql"
- // mongoBin is the Mongo client binary name.
- mongoBin = "mongo"
-)
diff --git a/tool/tsh/db_test.go b/tool/tsh/db_test.go
index 542b466916377..70ae4c6c01311 100644
--- a/tool/tsh/db_test.go
+++ b/tool/tsh/db_test.go
@@ -21,6 +21,7 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/pem"
+ "errors"
"io/ioutil"
"os"
"path/filepath"
@@ -325,3 +326,190 @@ func decodePEM(pemPath string) (certs []pem.Block, keys []pem.Block, err error)
}
return certs, keys, nil
}
+
+// fakeExec implements execer interface for mocking purposes.
+type fakeExec struct {
+ // execOutput maps binary name and output that should be returned on RunCommand().
+ // Map is also being used to check if a binary exist. Command line args are not supported.
+ execOutput map[string][]byte
+}
+
+func (f fakeExec) RunCommand(cmd string, _ ...string) ([]byte, error) {
+ out, found := f.execOutput[cmd]
+ if !found {
+ return nil, errors.New("binary not found")
+ }
+
+ return out, nil
+}
+
+func (f fakeExec) LookPath(path string) (string, error) {
+ if _, found := f.execOutput[path]; found {
+ return "", nil
+ }
+ return "", trace.NotFound("not found")
+}
+
+func TestCliCommandBuilderGetConnectCommand(t *testing.T) {
+ conf := &CLIConf{
+ HomePath: t.TempDir(),
+ Proxy: "proxy",
+ UserHost: "localhost",
+ SiteName: "db.example.com",
+ }
+
+ tc, err := makeClient(conf, true)
+ require.NoError(t, err)
+
+ profile := &client.ProfileStatus{
+ Name: "example.com",
+ Username: "bob",
+ Dir: "/tmp",
+ }
+
+ tests := []struct {
+ name string
+ dbProtocol string
+ execer *fakeExec
+ cmd []string
+ wantErr bool
+ }{
+ {
+ name: "postgres",
+ dbProtocol: defaults.ProtocolPostgres,
+ cmd: []string{"psql",
+ "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/certs.pem&" +
+ "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" +
+ "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"},
+ wantErr: false,
+ },
+ {
+ name: "cockroach",
+ dbProtocol: defaults.ProtocolCockroachDB,
+ execer: &fakeExec{
+ execOutput: map[string][]byte{
+ "cockroach": []byte(""),
+ },
+ },
+ cmd: []string{"cockroach", "sql", "--url",
+ "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/certs.pem&" +
+ "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" +
+ "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"},
+ wantErr: false,
+ },
+ {
+ name: "cockroach psql fallback",
+ dbProtocol: defaults.ProtocolCockroachDB,
+ execer: &fakeExec{},
+ cmd: []string{"psql",
+ "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/certs.pem&" +
+ "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" +
+ "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"},
+ wantErr: false,
+ },
+ {
+ name: "mariadb",
+ dbProtocol: defaults.ProtocolMySQL,
+ execer: &fakeExec{
+ execOutput: map[string][]byte{
+ "mariadb": []byte(""),
+ },
+ },
+ cmd: []string{"mariadb",
+ "--user", "myUser",
+ "--database", "mydb",
+ "--port", "12345",
+ "--host", "localhost",
+ "--protocol", "TCP",
+ "--ssl-key", "/tmp/keys/example.com/bob",
+ "--ssl-ca", "/tmp/keys/example.com/certs.pem",
+ "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem",
+ "--ssl-verify-server-cert"},
+ wantErr: false,
+ },
+ {
+ name: "mysql by mariadb",
+ dbProtocol: defaults.ProtocolMySQL,
+ execer: &fakeExec{
+ execOutput: map[string][]byte{
+ "mysql": []byte("mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2"),
+ },
+ },
+ cmd: []string{"mysql",
+ "--user", "myUser",
+ "--database", "mydb",
+ "--port", "12345",
+ "--host", "localhost",
+ "--protocol", "TCP",
+ "--ssl-key", "/tmp/keys/example.com/bob",
+ "--ssl-ca", "/tmp/keys/example.com/certs.pem",
+ "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem",
+ "--ssl-verify-server-cert"},
+ wantErr: false,
+ },
+ {
+ name: "mysql by oracle",
+ dbProtocol: defaults.ProtocolMySQL,
+ execer: &fakeExec{
+ execOutput: map[string][]byte{
+ "mysql": []byte("Ver 8.0.27-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))"),
+ },
+ },
+ cmd: []string{"mysql",
+ "--defaults-group-suffix=_db.example.com-mysql",
+ "--user", "myUser",
+ "--database", "mydb",
+ "--port", "12345",
+ "--host", "localhost",
+ "--protocol", "TCP"},
+ wantErr: false,
+ },
+ {
+ name: "no mysql nor mariadb",
+ dbProtocol: defaults.ProtocolMySQL,
+ execer: &fakeExec{
+ execOutput: map[string][]byte{},
+ },
+ cmd: []string{},
+ wantErr: true,
+ },
+ {
+ name: "mongodb",
+ dbProtocol: defaults.ProtocolMongoDB,
+ cmd: []string{"mongo",
+ "--host", "localhost",
+ "--port", "12345",
+ "--ssl",
+ "--sslPEMKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem",
+ "mydb"},
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ database := &tlsca.RouteToDatabase{
+ Protocol: tt.dbProtocol,
+ Database: "mydb",
+ Username: "myUser",
+ ServiceName: "mysql",
+ }
+
+ c := newCmdBuilder(tc, profile, database, WithLocalProxy("localhost", 12345, ""))
+ c.exe = tt.execer
+ got, err := c.getConnectCommand()
+ if tt.wantErr {
+ if err == nil {
+ t.Errorf("getConnectCommand() should return an error, but it didn't")
+ }
+ return
+ }
+
+ require.NoError(t, err)
+ require.Equal(t, tt.cmd, got.Args)
+ })
+ }
+}
diff --git a/tool/tsh/dbcmd.go b/tool/tsh/dbcmd.go
new file mode 100644
index 0000000000000..440cd0ae3ed02
--- /dev/null
+++ b/tool/tsh/dbcmd.go
@@ -0,0 +1,279 @@
+/*
+
+ 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 main
+
+import (
+ "fmt"
+ "os/exec"
+ "strconv"
+ "strings"
+
+ "github.com/gravitational/teleport/lib/client"
+ "github.com/gravitational/teleport/lib/client/db"
+ "github.com/gravitational/teleport/lib/client/db/mysql"
+ "github.com/gravitational/teleport/lib/client/db/postgres"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/tlsca"
+ "github.com/gravitational/trace"
+)
+
+const (
+ // postgresBin is the Postgres client binary name.
+ postgresBin = "psql"
+ // cockroachBin is the Cockroach client binary name.
+ cockroachBin = "cockroach"
+ // mysqlBin is the MySQL client binary name.
+ mysqlBin = "mysql"
+ // mariadbBin is the MariaDB client binary name.
+ mariadbBin = "mariadb"
+ // mongoBin is the Mongo client binary name.
+ mongoBin = "mongo"
+)
+
+// execer is an abstraction of Go's exec module, as this one doesn't specify any interfaces.
+// This interface exists only to enable mocking.
+type execer interface {
+ // RunCommand runs a system command.
+ RunCommand(name string, arg ...string) ([]byte, error)
+ // LookPath returns a full path to a binary if this one is found in system PATH,
+ // error otherwise.
+ LookPath(file string) (string, error)
+}
+
+// systemExecer implements execer interface by using Go exec module.
+type systemExecer struct{}
+
+// RunCommand is a wrapper for exec.Command(...).Output()
+func (s systemExecer) RunCommand(name string, arg ...string) ([]byte, error) {
+ return exec.Command(name, arg...).Output()
+}
+
+// LookPath is a wrapper for exec.LookPath(...)
+func (s systemExecer) LookPath(file string) (string, error) {
+ return exec.LookPath(file)
+}
+
+type cliCommandBuilder struct {
+ tc *client.TeleportClient
+ profile *client.ProfileStatus
+ db *tlsca.RouteToDatabase
+ host string
+ port int
+ options connectionCommandOpts
+
+ exe execer
+}
+
+func newCmdBuilder(tc *client.TeleportClient, profile *client.ProfileStatus,
+ db *tlsca.RouteToDatabase, opts ...ConnectCommandFunc,
+) *cliCommandBuilder {
+ var options connectionCommandOpts
+ for _, opt := range opts {
+ opt(&options)
+ }
+
+ // In TLS routing mode a local proxy is started on demand so connect to it.
+ host, port := tc.DatabaseProxyHostPort(*db)
+ if options.localProxyPort != 0 && options.localProxyHost != "" {
+ host = options.localProxyHost
+ port = options.localProxyPort
+ }
+
+ return &cliCommandBuilder{
+ tc: tc,
+ profile: profile,
+ db: db,
+ host: host,
+ port: port,
+ options: options,
+
+ exe: &systemExecer{},
+ }
+}
+
+func (c *cliCommandBuilder) getConnectCommand() (*exec.Cmd, error) {
+ switch c.db.Protocol {
+ case defaults.ProtocolPostgres:
+ return c.getPostgresCommand(), nil
+
+ case defaults.ProtocolCockroachDB:
+ return c.getCockroachCommand(), nil
+
+ case defaults.ProtocolMySQL:
+ return c.getMySQLCommand()
+
+ case defaults.ProtocolMongoDB:
+ return c.getMongoCommand(), nil
+ }
+
+ return nil, trace.BadParameter("unsupported database protocol: %v", c.db)
+}
+
+func (c *cliCommandBuilder) getPostgresCommand() *exec.Cmd {
+ return exec.Command(postgresBin,
+ postgres.GetConnString(db.New(c.tc, *c.db, *c.profile, c.host, c.port)))
+}
+
+func (c *cliCommandBuilder) getCockroachCommand() *exec.Cmd {
+ // If cockroach CLI client is not available, fallback to psql.
+ if _, err := c.exe.LookPath(cockroachBin); err != nil {
+ log.Debugf("Couldn't find %q client in PATH, falling back to %q: %v.",
+ cockroachBin, postgresBin, err)
+ return exec.Command(postgresBin,
+ postgres.GetConnString(db.New(c.tc, *c.db, *c.profile, c.host, c.port)))
+ }
+ return exec.Command(cockroachBin, "sql", "--url",
+ postgres.GetConnString(db.New(c.tc, *c.db, *c.profile, c.host, c.port)))
+}
+
+// getMySQLCommonCmdOpts returns common command line arguments for mysql and mariadb.
+// Currently, the common options are: user, database, host, port and protocol.
+func (c *cliCommandBuilder) getMySQLCommonCmdOpts() []string {
+ args := make([]string, 0)
+ if c.db.Username != "" {
+ args = append(args, "--user", c.db.Username)
+ }
+ if c.db.Database != "" {
+ args = append(args, "--database", c.db.Database)
+ }
+
+ if c.options.localProxyPort != 0 {
+ args = append(args, "--port", strconv.Itoa(c.options.localProxyPort))
+ args = append(args, "--host", c.options.localProxyHost)
+ // MySQL CLI treats localhost as a special value and tries to use Unix Domain Socket for connection
+ // To enforce TCP connection protocol needs to be explicitly specified.
+ if c.options.localProxyHost == "localhost" {
+ args = append(args, "--protocol", "TCP")
+ }
+ }
+
+ return args
+}
+
+// getMariaDBArgs returns arguments unique for mysql cmd shipped by MariaDB and mariadb cmd. Common options for mysql
+// between Oracle and MariaDB version are covered by getMySQLCommonCmdOpts().
+func (c *cliCommandBuilder) getMariaDBArgs() []string {
+ args := c.getMySQLCommonCmdOpts()
+ sslCertPath := c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName)
+
+ args = append(args, []string{"--ssl-key", c.profile.KeyPath()}...)
+ args = append(args, []string{"--ssl-ca", c.profile.CACertPath()}...)
+ args = append(args, []string{"--ssl-cert", sslCertPath}...)
+
+ // Flag below verifies "Common Name" check on the certificate provided by the server.
+ // This option is disabled by default.
+ if !c.tc.InsecureSkipVerify {
+ args = append(args, "--ssl-verify-server-cert")
+ }
+
+ return args
+}
+
+// getMySQLOracleCommand returns arguments unique for mysql cmd shipped by Oracle. Common options between
+// Oracle and MariaDB version are covered by getMySQLCommonCmdOpts().
+func (c *cliCommandBuilder) getMySQLOracleCommand() *exec.Cmd {
+ args := c.getMySQLCommonCmdOpts()
+
+ // defaults-group-suffix must be first.
+ groupSuffix := []string{fmt.Sprintf("--defaults-group-suffix=_%v-%v", c.tc.SiteName, c.db.ServiceName)}
+ args = append(groupSuffix, args...)
+
+ // override the ssl-mode from a config file is --insecure flag is provided to 'tsh db connect'.
+ if c.tc.InsecureSkipVerify {
+ args = append(args, fmt.Sprintf("--ssl-mode=%s", mysql.MySQLSSLModeVerifyCA))
+ }
+
+ return exec.Command(mysqlBin, args...)
+}
+
+// getMySQLCommand returns mariadb command if the binary is on the path. Otherwise,
+// mysql command is returned. Both mysql versions (MariaDB and Oracle) are supported.
+func (c *cliCommandBuilder) getMySQLCommand() (*exec.Cmd, error) {
+ // Check if mariadb client is available. Prefer it over mysql client even if connecting to MySQL server.
+ if c.isMariaDBBinAvailable() {
+ args := c.getMariaDBArgs()
+ return exec.Command(mariadbBin, args...), nil
+ }
+
+ // Check for mysql binary. Return with error as mysql and mariadb are missing. There is nothing else we can do here.
+ if !c.isMySQLBinAvailable() {
+ return nil, trace.NotFound("neither \"mysql\" nor \"mariadb\" were found")
+ }
+
+ // Check which flavor is installed. Otherwise, we don't know which ssl flag to use.
+ // At the moment of writing mysql binary shipped by Oracle and MariaDB accept different ssl parameters and have the same name.
+ mySQLMariaDBFlavor, err := c.isMySQLBinMariaDBFlavor()
+ if mySQLMariaDBFlavor && err == nil {
+ args := c.getMariaDBArgs()
+ return exec.Command(mysqlBin, args...), nil
+ }
+
+ // Either we failed to check the flavor or binary comes from Oracle. Regardless return mysql/Oracle command.
+ return c.getMySQLOracleCommand(), nil
+}
+
+// isMariaDBBinAvailable returns true if "mariadb" binary is found in the system PATH.
+func (c *cliCommandBuilder) isMariaDBBinAvailable() bool {
+ _, err := c.exe.LookPath(mariadbBin)
+ return err == nil
+}
+
+// isMySQLBinAvailable returns true if "mysql" binary is found in the system PATH.
+func (c *cliCommandBuilder) isMySQLBinAvailable() bool {
+ _, err := c.exe.LookPath(mysqlBin)
+ return err == nil
+}
+
+// isMySQLBinMariaDBFlavor checks if mysql binary comes from Oracle or MariaDB.
+// true is returned when binary comes from MariaDB, false when from Oracle.
+func (c *cliCommandBuilder) isMySQLBinMariaDBFlavor() (bool, error) {
+ // Check if mysql comes from Oracle or MariaDB
+ mysqlVer, err := c.exe.RunCommand(mysqlBin, "--version")
+ if err != nil {
+ // Looks like incorrect mysql installation.
+ return false, trace.Wrap(err)
+ }
+
+ // Check which flavor is installed. Otherwise, we don't know which ssl flag to use.
+ // Example output:
+ // Oracle:
+ // mysql Ver 8.0.27-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))
+ // MariaDB:
+ // mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
+ return strings.Contains(strings.ToLower(string(mysqlVer)), "mariadb"), nil
+}
+
+func (c *cliCommandBuilder) getMongoCommand() *exec.Cmd {
+ args := []string{
+ "--host", c.host,
+ "--port", strconv.Itoa(c.port),
+ "--ssl",
+ "--sslPEMKeyFile", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName),
+ }
+
+ if c.options.caPath != "" {
+ // caPath is set only if mongo connects to the Teleport Proxy via ALPN SNI Local Proxy
+ // and connection is terminated by proxy identity certificate.
+ args = append(args, []string{"--sslCAFile", c.options.caPath}...)
+ }
+ if c.db.Database != "" {
+ args = append(args, c.db.Database)
+ }
+ return exec.Command(mongoBin, args...)
+}