From bd4e854027f8454687cc777fe1ce5c2a0f889aaa Mon Sep 17 00:00:00 2001 From: Tobias Grieger Date: Thu, 3 Sep 2020 14:19:52 +0200 Subject: [PATCH] Test connecting tenants via password over TLS through proxy This commit adds infrastructure for testing secure connections through the multi-tenancy proxy to a standalone SQL server. This works "out of the box" for essentially those tests for which we previously had client certificates working. The tests that fail here are GoPG, Hibernate, Sequelize, Django and they all fail for the basic reason that they don't support secure query strings, at least not in the way they are supplied right now. To *really* find out whether they have issues with multi-tenancy (or even client certs), we need to customize the way they receive the connection parameters. This is not something I plan to investigate. Closes https://github.com/cockroachdb/cockroach/issues/52413. --- go.mod | 2 +- go.sum | 7 +- testing/main_test.go | 180 +++++++++++++++++++++++++++++++------------ 3 files changed, 136 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 15a548c06f..c3a1ec38b5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cockroachdb/examples-orms go 1.13 require ( - github.com/cockroachdb/cockroach-go/v2 v2.0.6 + github.com/cockroachdb/cockroach-go/v2 v2.0.7 github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec // indirect github.com/go-pg/pg/v9 v9.1.6 github.com/go-sql-driver/mysql v1.5.0 // indirect diff --git a/go.sum b/go.sum index 8a54c4ac71..7e58a7c316 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.0.6 h1:7JKIGouLq/hP+rxHIZ1emZ0J3AXPt8DE7yU/xw8JZPc= -github.com/cockroachdb/cockroach-go/v2 v2.0.6/go.mod h1:nkf7rUmgPdawp3EwRjXIumihI2AYg9usGNWbJ2hsJqI= +github.com/cockroachdb/cockroach-go/v2 v2.0.7-0.20200903114400-4bf206d91533 h1:FrTWuMCYMkwme38Us67F6VK5xSF1/iLlasSJR8EIhIg= +github.com/cockroachdb/cockroach-go/v2 v2.0.7-0.20200903114400-4bf206d91533/go.mod h1:nkf7rUmgPdawp3EwRjXIumihI2AYg9usGNWbJ2hsJqI= +github.com/cockroachdb/cockroach-go/v2 v2.0.7 h1:Xkv9PbnvBrZjxRdW7l6GLz76bxAE/xaro+SGTyLz6gQ= +github.com/cockroachdb/cockroach-go/v2 v2.0.7/go.mod h1:nkf7rUmgPdawp3EwRjXIumihI2AYg9usGNWbJ2hsJqI= github.com/codemodus/kace v0.5.1 h1:4OCsBlE2c/rSJo375ggfnucv9eRzge/U5LrrOZd47HA= github.com/codemodus/kace v0.5.1/go.mod h1:coddaHoX1ku1YFSe4Ip0mL9kQjJvKkzb9CfIdG1YR04= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -187,6 +189,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f h1:ESK9Jb5JOE+y4u+ozMQeXfMHwEHm6zVbaDQkeaj6wI4= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/testing/main_test.go b/testing/main_test.go index f21f1af1bc..b1c60e07f4 100644 --- a/testing/main_test.go +++ b/testing/main_test.go @@ -47,14 +47,19 @@ type tenantServer interface { } // newServer creates a new cockroachDB server. -func newServer(t *testing.T, insecure bool) testserver.TestServer { +func newServer(t *testing.T, auth authMode) testserver.TestServer { t.Helper() var ts testserver.TestServer var err error - if insecure { - ts, err = testserver.NewTestServer() - } else { + switch auth { + case authClientCert: ts, err = testserver.NewTestServer(testserver.SecureOpt()) + case authPassword: + ts, err = testserver.NewTestServer(testserver.SecureOpt(), testserver.RootPasswordOpt("hunter2")) + case authInsecure: + ts, err = testserver.NewTestServer() + default: + err = fmt.Errorf("unknown authMode %d", auth) } if err != nil { t.Fatal(err) @@ -64,9 +69,9 @@ func newServer(t *testing.T, insecure bool) testserver.TestServer { // newTenant creates a new SQL Tenant pointed at the given TestServer. See // TestServer.NewTenantServer for more information. -func newTenant(t *testing.T, ts testserver.TestServer) testserver.TestServer { +func newTenant(t *testing.T, ts testserver.TestServer, proxy bool) testserver.TestServer { t.Helper() - tenant, err := ts.(tenantServer).NewTenantServer(false /* proxy */) + tenant, err := ts.(tenantServer).NewTenantServer(proxy) if err != nil { t.Fatal(err) } @@ -193,17 +198,39 @@ var minRequiredVersionsByORMName = map[string]struct { }, } +type authMode byte + +const ( + // Use client certs. When testing tenants, does not use the proxy (as the proxy does not support client certs). + authClientCert authMode = iota + // Use password auth. When testing tenants, tests through a proxy. + authPassword + // Use --insecure. When testing tenants, does not use the proxy (as the proxy does not support insecure connections). + authInsecure + + authModeSentinel // sentinel to iterate over all modes +) + +func (mode authMode) String() string { + switch mode { + case authClientCert: + return "client-cert" + case authPassword: + return "password" + case authInsecure: + return "insecure" + default: + return "unknown" + } +} + type testInfo struct { language, orm string tableNames testTableNames // defaults to defaultTestTableNames columnNames testColumnNames // defaults to defaultTestColumnNames - // insecure is set if ORM does not handle secure servers (client certs). - // In that case, we start an insecure server (and don't test in tenant - // mode). - insecure bool } -func testORM(t *testing.T, info testInfo) { +func testORM(t *testing.T, info testInfo, auth authMode) { if info.tableNames == (testTableNames{}) { info.tableNames = defaultTestTableNames } @@ -222,7 +249,7 @@ func testORM(t *testing.T, info testInfo) { } var testCases []testCase { - ts := newServer(t, info.insecure) + ts := newServer(t, auth) db, dbURL, stopDB := startServerWithApplication(t, ts, app) defer stopDB() @@ -268,11 +295,19 @@ FROM t.Fatalf("unable to read cluster version: %s", err) } if tenantsSupported { - tenant := newTenant(t, ts) + // Connect to the tenant through the SQL proxy, which is only supported + // when using secure+password auth. (The proxy does not support client + // certs or insecure connections). + proxySupported := auth == authPassword + name := "RegularTenant" + if proxySupported { + name += "ThroughProxy" + } + tenant := newTenant(t, ts, proxySupported) db, dbURL, stopDB := startServerWithApplication(t, tenant, app) defer stopDB() testCases = append(testCases, testCase{ - name: "RegularTenant", + name: name, db: db, dbURL: dbURL, }) @@ -368,66 +403,111 @@ FROM } } +func testORMForAuthModesExcept(t *testing.T, info testInfo, skips map[authMode]string /* mode -> reason */) { + for auth := authMode(0); auth < authModeSentinel; auth++ { + t.Run(fmt.Sprint(auth), func(t *testing.T) { + if msg := skips[auth]; msg != "" { + t.Skip(msg) + } + testORM(t, info, auth) + }) + } +} + +func nothingSkipped() map[authMode]string { return nil } + func TestGORM(t *testing.T) { - testORM(t, testInfo{language: "go", orm: "gorm"}) + testORMForAuthModesExcept(t, testInfo{language: "go", orm: "gorm"}, nothingSkipped()) } func TestGOPG(t *testing.T) { - testORM(t, testInfo{ - language: "go", - orm: "gopg", - // GoPG does not support client certs: - // https://github.com/go-pg/pg/blob/v10/options.go - // If we set up a secure deployment and went through the proxy, it would work (or should anyway), but only - // via the 'database' parameter; GoPG also does not support the 'options' parameter. - insecure: true, - }) + testORMForAuthModesExcept(t, + testInfo{language: "go", orm: "gopg"}, + map[authMode]string{ + // https://github.com/go-pg/pg/blob/v10/options.go + // If we set up a secure deployment and went through the proxy, it would work (or should anyway), but only + // via the 'database' parameter; GoPG also does not support the 'options' parameter. + // + // pg: options other than 'sslmode', 'application_name' and 'connect_timeout' are not supported + authClientCert: "GoPG does not support custom root cert", + authPassword: "GoPG does not support custom root cert", + }) } func TestHibernate(t *testing.T) { - testORM(t, testInfo{ - language: "java", - orm: "hibernate", - // Possibly does not unescape the path correctly: - // Caused by: java.io.FileNotFoundException: - // %2Ftmp%2Fcockroach-testserver913095208%2Fcerts%2Fca.crt (No such file or directory) - insecure: true, - }) + testORMForAuthModesExcept(t, + testInfo{language: "java", orm: "hibernate"}, + map[authMode]string{ + // Driver does not unescape the path correctly: + // Caused by: java.io.FileNotFoundException: + // %2Ftmp%2Fcockroach-testserver913095208%2Fcerts%2Fca.crt (No such file or directory) + // + // Furthermore, if we preprocess the query string via + // + // tc.dbURL.RawQuery, err = url.QueryUnescape(tc.dbURL.RawQuery) + // + // then we run into + // https://github.com/dbeaver/dbeaver/issues/1835 + // because hibernate expects the key in DER format, but it is PEM. + authClientCert: "needs DER format and unescaped query string", + // Doesn't seem to understand connection strings. + // Caused by: java.net.UnknownHostException: root:hunter2@localhost + authPassword: "needs custom setup for password support", + }, + ) } func TestSequelize(t *testing.T) { - testORM(t, testInfo{ - language: "node", - orm: "sequelize", - // Requires bespoke code to actually use SSL, see: - // https://github.com/sequelize/sequelize/issues/10015 - insecure: true, - }) + testORMForAuthModesExcept(t, + testInfo{language: "node", orm: "sequelize"}, + map[authMode]string{ + // Requires bespoke code to actually use SSL, see: + // https://github.com/sequelize/sequelize/issues/10015 + authClientCert: "needs custom SSL setup", + authPassword: "needs custom SSL setup", + }, + ) } func TestSQLAlchemy(t *testing.T) { - testORM(t, testInfo{ - language: "python", - orm: "sqlalchemy", - }) + testORMForAuthModesExcept(t, testInfo{language: "python", orm: "sqlalchemy"}, nothingSkipped()) } func TestDjango(t *testing.T) { - testORM(t, testInfo{ + testORMForAuthModesExcept(t, testInfo{ language: "python", orm: "django", tableNames: djangoTestTableNames, columnNames: djangoTestColumnNames, - // No support for client certs (at least not via the query string). - // psycopg2.OperationalError: fe_sendauth: no password supplied - insecure: true, - }) + }, + map[authMode]string{ + // No support for client certs (at least not via the query string). + // psycopg2.OperationalError: fe_sendauth: no password supplied + authClientCert: "client certs via query string unsupported", + // Ditto, + // psycopg2.OperationalError: fe_sendauth: no password supplied + authPassword: "password via query string unsupported", + }, + ) } +// TODO(rafiss): why can't I run the ActiveRecords tests manually +// with this invocation? +// +// ./docker.sh make deps +// ./docker.sh go test -v -run ActiveRecord ./testing +// +// It always fails opaquely like this (after some normal-looking output): +// +// => Run `rails server -h` for more startup options +// Exiting +// make: *** [Makefile:23: start] Error 1 +// + func TestActiveRecord(t *testing.T) { - testORM(t, testInfo{language: "ruby", orm: "activerecord"}) + testORMForAuthModesExcept(t, testInfo{language: "ruby", orm: "activerecord"}, nothingSkipped()) } func TestActiveRecord4(t *testing.T) { - testORM(t, testInfo{language: "ruby", orm: "ar4"}) + testORMForAuthModesExcept(t, testInfo{language: "ruby", orm: "ar4"}, nothingSkipped()) }