From cc632e8193b460edfe13a758eb25d19e927c1c20 Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:53:33 -0700 Subject: [PATCH] Fix SSH tunnel leaks when testing connections (#24446) * Fix SSH tunnel leaks when testing connections * Appease kondo =( * Add some more NOTES --- .clj-kondo/config.edn | 79 ++++++++++--------- docs/developers-guide/driver-changelog.md | 8 ++ .../src/metabase/driver/snowflake.clj | 8 +- src/metabase/driver.clj | 6 +- src/metabase/driver/mysql.clj | 18 ++--- src/metabase/driver/sql_jdbc/connection.clj | 36 ++++++--- src/metabase/util/ssh.clj | 19 +++-- test/metabase/util/ssh_test.clj | 12 +-- 8 files changed, 109 insertions(+), 77 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index c28c693b4a2..9e86bf61672 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -342,45 +342,48 @@ saml20-clj.core saml toucan.db db toucan.models models}}} - :lint-as {metabase.api.common/let-404 clojure.core/let - metabase.db.data-migrations/defmigration clojure.core/def - metabase.query-processor.error-type/deferror clojure.core/def - metabase.models.setting/defsetting clj-kondo.lint-as/def-catch-all - metabase.mbql.schema.macros/defclause clj-kondo.lint-as/def-catch-all - metabase.public-settings.premium-features/defenterprise clj-kondo.lint-as/def-catch-all - metabase.public-settings.premium-features/defenterprise-schema clj-kondo.lint-as/def-catch-all - metabase.public-settings.premium-features/define-premium-feature clojure.core/def - metabase.sync.util/sum-for clojure.core/for - metabase.sync.util/with-emoji-progress-bar clojure.core/let - metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info clojure.core/fn - metabase.util.files/with-open-path-to-resource clojure.core/let - metabase.util.ssh/with-ssh-tunnel clojure.core/let - metabase.db.liquibase/with-liquibase clojure.core/let - metabase.models.setting.multi-setting/define-multi-setting clojure.core/def - metabase.integrations.ldap/with-ldap-connection clojure.core/fn - metabase.test/are+ clojure.test/are - metabase.test/defdataset clojure.core/def - metabase.test/with-temp-file clojure.core/let - metabase.test/with-open-channels clojure.core/let - metabase.test/with-user-in-groups clojure.core/let - metabase.test.data.interface/defdataset clojure.core/def - metabase.test.data.interface/defdataset-edn clojure.core/def - metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let - metabase.driver.mongo.util/with-mongo-connection clojure.core/let - metabase.driver.mongo.query-processor/mongo-let clojure.core/let - toucan.db/with-call-counting clojure.core/fn - potemkin.types/defprotocol+ clojure.core/defprotocol - potemkin/defprotocol+ clojure.core/defprotocol - potemkin.types/defrecord+ clojure.core/defrecord - potemkin/defrecord+ clojure.core/defrecord - potemkin.types/deftype+ clojure.core/deftype - potemkin/deftype+ clojure.core/deftype - clojurewerkz.quartzite.jobs/defjob clojure.core/defn - honeysql.util/defalias clojure.core/def - honeysql.helpers/defhelper clj-kondo.lint-as/def-catch-all - clojure.core.logic/defne clj-kondo.lint-as/def-catch-all - monger.operators/defoperator clojure.core/def} + :lint-as + {clojure.core.logic/defne clj-kondo.lint-as/def-catch-all + clojurewerkz.quartzite.jobs/defjob clojure.core/defn + honeysql.helpers/defhelper clj-kondo.lint-as/def-catch-all + honeysql.util/defalias clojure.core/def + metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let + metabase.api.common/let-404 clojure.core/let + metabase.db.data-migrations/defmigration clojure.core/def + metabase.db.liquibase/with-liquibase clojure.core/let + metabase.driver.mongo.query-processor/mongo-let clojure.core/let + metabase.driver.mongo.util/with-mongo-connection clojure.core/let + metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection clojure.core/let + metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info clojure.core/fn + metabase.integrations.ldap/with-ldap-connection clojure.core/fn + metabase.mbql.schema.macros/defclause clj-kondo.lint-as/def-catch-all + metabase.models.setting.multi-setting/define-multi-setting clojure.core/def + metabase.models.setting/defsetting clj-kondo.lint-as/def-catch-all + metabase.public-settings.premium-features/defenterprise clj-kondo.lint-as/def-catch-all + metabase.public-settings.premium-features/defenterprise-schema clj-kondo.lint-as/def-catch-all + metabase.public-settings.premium-features/define-premium-feature clojure.core/def + metabase.query-processor.error-type/deferror clojure.core/def + metabase.sync.util/sum-for clojure.core/for + metabase.sync.util/with-emoji-progress-bar clojure.core/let + metabase.test.data.interface/defdataset clojure.core/def + metabase.test.data.interface/defdataset-edn clojure.core/def + metabase.test/are+ clojure.test/are + metabase.test/defdataset clojure.core/def + metabase.test/with-open-channels clojure.core/let + metabase.test/with-temp-file clojure.core/let + metabase.test/with-user-in-groups clojure.core/let + metabase.util.files/with-open-path-to-resource clojure.core/let + metabase.util.ssh/with-ssh-tunnel clojure.core/let + monger.operators/defoperator clojure.core/def + potemkin.types/defprotocol+ clojure.core/defprotocol + potemkin.types/defrecord+ clojure.core/defrecord + potemkin.types/deftype+ clojure.core/deftype + potemkin/defprotocol+ clojure.core/defprotocol + potemkin/defrecord+ clojure.core/defrecord + potemkin/deftype+ clojure.core/deftype + toucan.db/with-call-counting clojure.core/fn} + :hooks {:analyze-call {metabase.api.common/defendpoint hooks.metabase.api.common/defendpoint diff --git a/docs/developers-guide/driver-changelog.md b/docs/developers-guide/driver-changelog.md index 887f8053af8..c693e3eff27 100644 --- a/docs/developers-guide/driver-changelog.md +++ b/docs/developers-guide/driver-changelog.md @@ -4,6 +4,14 @@ title: Driver interface changelog # Driver Interface Changelog +## Metabase 0.45.0 + +- `metabase.driver.sql-jdbc.connection/details->connection-spec-for-testing-connection` has been removed in Metabase + 0.45.0, because it leaked SSH tunnels. See [#24445](https://github.com/metabase/metabase/issues/24445). If you are + using this function, please update your code to use + `metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection` instead, which properly cleans up + after itself. + ## Metabase 0.43.0 - The `:expressions` map in an MBQL query now uses strings as keys rather than keywords (see diff --git a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj index 5b533ce6590..2798aa7c759 100644 --- a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj +++ b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj @@ -313,10 +313,10 @@ (defmethod driver/can-connect? :snowflake [driver {:keys [db], :as details}] (and ((get-method driver/can-connect? :sql-jdbc) driver details) - (let [spec (sql-jdbc.conn/details->connection-spec-for-testing-connection driver details) - sql (format "SHOW OBJECTS IN DATABASE \"%s\";" db)] - (jdbc/query spec sql) - true))) + (sql-jdbc.conn/with-connection-spec-for-testing-connection [spec [driver details]] + (let [sql (format "SHOW OBJECTS IN DATABASE \"%s\";" db)] + (jdbc/query spec sql) + true)))) (defmethod driver/normalize-db-details :snowflake [_ database] diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index 0a059649b91..7b34e2789c0 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -670,7 +670,11 @@ (defmulti incorporate-ssh-tunnel-details "A multimethod for driver-specific behavior required to incorporate details for an opened SSH tunnel into the DB details. In most cases, this will simply involve updating the :host and :port (to point to the tunnel entry point, - instead of the backing database server), but some drivers may have more specific behavior." + instead of the backing database server), but some drivers may have more specific behavior. + + WARNING! Implementations of this method may create new SSH tunnels, which need to be cleaned up. DO NOT USE THIS + METHOD DIRECTLY UNLESS YOU ARE GOING TO BE CLEANING UP ANY CREATED TUNNELS! Instead, you probably want to + use [[metabase.util.ssh/with-ssh-tunnel]]. See #24445 for more information." {:added "0.39.0" :arglists '([driver db-details])} dispatch-on-uninitialized-driver :hierarchy #'hierarchy) diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj index b1625da9f1b..2eb5a0b9349 100644 --- a/src/metabase/driver/mysql.clj +++ b/src/metabase/driver/mysql.clj @@ -69,19 +69,19 @@ (if (mariadb? metadata) min-supported-mariadb-version min-supported-mysql-version))) (defn- warn-on-unsupported-versions [driver details] - (let [jdbc-spec (sql-jdbc.conn/details->connection-spec-for-testing-connection driver details)] + (sql-jdbc.conn/with-connection-spec-for-testing-connection [jdbc-spec [driver details]] (jdbc/with-db-metadata [metadata jdbc-spec] (when (unsupported-version? metadata) (log/warn (u/format-color 'red - (str - "\n\n********************************************************************************\n" - (trs "WARNING: Metabase only officially supports MySQL {0}/MariaDB {1} and above." - min-supported-mysql-version - min-supported-mariadb-version) - "\n" - (trs "All Metabase features may not work properly when using an unsupported version.") - "\n********************************************************************************\n"))))))) + (str + "\n\n********************************************************************************\n" + (trs "WARNING: Metabase only officially supports MySQL {0}/MariaDB {1} and above." + min-supported-mysql-version + min-supported-mariadb-version) + "\n" + (trs "All Metabase features may not work properly when using an unsupported version.") + "\n********************************************************************************\n"))))))) (defmethod driver/can-connect? :mysql [driver details] diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj index 09224fe7dc1..c81d478e847 100644 --- a/src/metabase/driver/sql_jdbc/connection.clj +++ b/src/metabase/driver/sql_jdbc/connection.clj @@ -249,24 +249,34 @@ ;;; | metabase.driver impls | ;;; +----------------------------------------------------------------------------------------------------------------+ -(defn details->connection-spec-for-testing-connection - "Return an appropriate JDBC connection spec to test whether a set of connection details is valid (i.e., implementing - `can-connect?`)." - [driver details] - (let [details-with-tunnel (driver/incorporate-ssh-tunnel-details - driver - (update details :port #(or % (default-ssh-tunnel-target-port driver))))] - (connection-details->spec driver details-with-tunnel))) +(defn do-with-connection-spec-for-testing-connection + "Impl for [[with-connection-spec-for-testing-connection]]." + [driver details f] + (let [details (update details :port #(or % (default-ssh-tunnel-target-port driver)))] + (ssh/with-ssh-tunnel [details-with-tunnel details] + (let [spec (connection-details->spec driver details-with-tunnel)] + (f spec))))) + +(defmacro with-connection-spec-for-testing-connection + "Execute `body` with an appropriate [[clojure.java.jdbc]] connection spec based on connection `details`. Handles SSH + tunneling as needed and properly cleans up after itself. + + (with-connection-spec-for-testing-connection [jdbc-spec [:my-driver conn-details]] + (do-something-with-spec jdbc-spec)" + {:added "0.45.0", :style/indent 1} + [[jdbc-spec-binding [driver details]] & body] + `(do-with-connection-spec-for-testing-connection ~driver ~details (^:once fn* [~jdbc-spec-binding] ~@body))) (defn can-connect-with-spec? - "Can we connect to a JDBC database with `clojure.java.jdbc` `jdbc-spec` and run a simple query?" + "Can we connect to a JDBC database with [[clojure.java.jdbc]] `jdbc-spec` and run a simple query?" [jdbc-spec] (let [[first-row] (jdbc/query jdbc-spec ["SELECT 1"]) [result] (vals first-row)] - (= 1 result))) + (= result 1))) (defn can-connect? - "Default implementation of `driver/can-connect?` for SQL JDBC drivers. Checks whether we can perform a simple `SELECT - 1` query." + "Default implementation of [[driver/can-connect?]] for SQL JDBC drivers. Checks whether we can perform a simple + `SELECT 1` query." [driver details] - (can-connect-with-spec? (details->connection-spec-for-testing-connection driver details))) + (with-connection-spec-for-testing-connection [jdbc-spec [driver details]] + (can-connect-with-spec? jdbc-spec))) diff --git a/src/metabase/util/ssh.clj b/src/metabase/util/ssh.clj index abb9ea49317..8fba4d37408 100644 --- a/src/metabase/util/ssh.clj +++ b/src/metabase/util/ssh.clj @@ -1,4 +1,7 @@ (ns metabase.util.ssh + "SSH tunnel support for JDBC-based DWs. TODO -- it seems like this code is JDBC-specific, or at least big parts of + this all. We should consider moving some or all of this code to a new namespace like + `metabase.driver.sql-jdbc.connection.ssh-tunnel` or something like that." (:require [clojure.tools.logging :as log] [metabase.driver :as driver] [metabase.public-settings :as public-settings] @@ -45,9 +48,9 @@ keypair (GenericUtils/head ids)] (.addPublicKeyIdentity session keypair)))) -(defn start-ssh-tunnel! +(defn- start-ssh-tunnel! "Opens a new ssh tunnel and returns the connection along with the dynamically assigned tunnel entrance port. It's the - callers responsibility to call `close-tunnel` on the returned connection object." + callers responsibility to call [[close-tunnel!]] on the returned connection object." [{:keys [^String tunnel-host ^Integer tunnel-port ^String tunnel-user tunnel-pass tunnel-private-key tunnel-private-key-passphrase host port]}] {:pre [(integer? port)]} @@ -102,8 +105,9 @@ details-with-tunnel) details)) +;; TODO Seems like this definitely belongs in [[metabase.driver.sql-jdbc.connection]] or something like that. (defmethod driver/incorporate-ssh-tunnel-details :sql-jdbc - [_ db-details] + [_driver db-details] (cond (not (use-ssh-tunnel? db-details)) ;; no ssh tunnel in use db-details @@ -118,6 +122,7 @@ "Close a running tunnel session" [details] (when (and (use-ssh-tunnel? details) (ssh-tunnel-open? details)) + (log/tracef "Closing SSH tunnel: %s" (:tunnel-session details)) (.close ^ClientSession (:tunnel-session details)))) (defn do-with-ssh-tunnel @@ -133,10 +138,12 @@ (log/trace (u/format-color 'cyan "<< CLOSED SSH TUNNEL >>"))))) (f details))) +;;; TODO -- I think `with-ssh-tunnel-details` or something like that would be a better name for this. Since it doesn't +;;; actually give you a tunnel. It just gives you connection details that include a tunnel in there. (defmacro with-ssh-tunnel "Starts an ssh tunnel, and binds the supplied name to a database details map with it's values adjusted to use the tunnel" - [[name details] & body] + [[details-binding details] & body] `(do-with-ssh-tunnel ~details - (fn [~name] - ~@body))) + (fn [~details-binding] + ~@body))) diff --git a/test/metabase/util/ssh_test.clj b/test/metabase/util/ssh_test.clj index 1bad54252be..02fefe5327e 100644 --- a/test/metabase/util/ssh_test.clj +++ b/test/metabase/util/ssh_test.clj @@ -119,7 +119,7 @@ ;; correct password (deftest connects-with-correct-password - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-password-port @@ -131,7 +131,7 @@ (deftest throws-exception-on-incorrect-password (is (thrown? org.apache.sshd.common.SshException - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-password-port @@ -142,7 +142,7 @@ ;; correct ssh key (deftest connects-with-correct-ssh-key (is (some? - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-publickey-port @@ -154,7 +154,7 @@ (deftest throws-exception-on-incorrect-ssh-key (is (thrown? org.apache.sshd.common.SshException - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-publickey-port @@ -165,7 +165,7 @@ ;; correct ssh key (deftest connects-with-correct-ssh-key-and-passphrase (is (some? - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-publickey-passphrase-port @@ -177,7 +177,7 @@ (deftest throws-exception-on-incorrect-ssh-key-and-passphrase (is (thrown? java.io.StreamCorruptedException - (ssh/start-ssh-tunnel! + (#'ssh/start-ssh-tunnel! {:tunnel-user ssh-username :tunnel-host "127.0.0.1" :tunnel-port ssh-mock-server-with-publickey-passphrase-port -- GitLab