From bae67b3c95427fdd351b821904835773ad9fc035 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Mon, 10 Jan 2022 20:12:31 -0500 Subject: [PATCH] Add support for MariaDB (#9409) Add support for MariaDB client and update documentation. --- docs/config.json | 2 +- docs/pages/database-access/faq.mdx | 2 +- docs/pages/database-access/guides.mdx | 4 +- .../guides/mysql-self-hosted.mdx | 38 ++- docs/pages/database-access/guides/rds.mdx | 2 +- docs/pages/database-access/introduction.mdx | 6 +- docs/pages/index.mdx | 2 +- lib/client/db/mysql/optionfile.go | 2 +- lib/defaults/defaults.go | 4 +- tool/tsh/db.go | 112 +------ tool/tsh/db_test.go | 188 ++++++++++++ tool/tsh/dbcmd.go | 279 ++++++++++++++++++ 12 files changed, 511 insertions(+), 130 deletions(-) create mode 100644 tool/tsh/dbcmd.go 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...) +}