diff --git a/frontend/src/metabase/containers/Form.jsx b/frontend/src/metabase/containers/Form.jsx index 2784d1299eca09970839baa2e8efb63fb37b4f5f..4a12d1a425c5a3ca717b984d9e5492c89aa2b0f9 100644 --- a/frontend/src/metabase/containers/Form.jsx +++ b/frontend/src/metabase/containers/Form.jsx @@ -181,11 +181,29 @@ export default class Form extends React.Component { formDef => makeFormObject(formDef), ); const getInitialValues = createSelector( - [getFormObject, (state, props) => props.initialValues || {}], - (formObject, initialValues) => ({ - ...formObject.initial(), - ...initialValues, - }), + [ + getFormObject, + (state, props) => props.initialValues || {}, + (state, props) => props.values || {}, + ], + (formObject, initialValues, values) => { + const formInitialValues = formObject.initial(values); + // merge nested fields: {details: {foo: 123}} + {details: {bar: 321}} => {details: {foo: 123, bar: 321}} + const merged = {}; + for (const k of Object.keys(initialValues)) { + if ( + typeof initialValues[k] === "object" && + typeof formInitialValues[k] === "object" + ) { + merged[k] = { ...formInitialValues[k], ...initialValues[k] }; + } + } + return { + ...initialValues, + ...formInitialValues, + ...merged, + }; + }, ); const getFieldNames = createSelector( [getFormObject, getInitialValues, (state, props) => props.values || {}], @@ -215,18 +233,16 @@ export default class Form extends React.Component { }; componentDidUpdate(prevProps: Props, prevState: State) { - if (!this.props.form) { - // HACK: when new fields are added they aren't initialized with their intialValues, so we have to force it here: - const newFields = _.difference( - Object.keys(this.state.inlineFields), - Object.keys(prevState.inlineFields), + // HACK: when new fields are added they aren't initialized with their intialValues, so we have to force it here: + const newFields = _.difference( + Object.keys(this.state.inlineFields), + Object.keys(prevState.inlineFields), + ); + if (newFields.length > 0) { + // $FlowFixMe: dispatch provided by connect + this.props.dispatch( + initialize(this.props.formName, this._getInitialValues(), newFields), ); - if (newFields.length > 0) { - // $FlowFixMe: dispatch provided by connect - this.props.dispatch( - initialize(this.props.formName, this._getInitialValues(), newFields), - ); - } } } @@ -314,6 +330,7 @@ export default class Form extends React.Component { return ( <ReduxFormComponent {...this.props} + overwriteOnInitialValuesChange={false} formObject={formObject} // redux-form props: form={formName} diff --git a/frontend/src/metabase/css/core/base.css b/frontend/src/metabase/css/core/base.css index 589676e79f98bd50ba497b118d1929539029d794..2526b7f14dc665e3878747c145d21806ca05c1a4 100644 --- a/frontend/src/metabase/css/core/base.css +++ b/frontend/src/metabase/css/core/base.css @@ -117,6 +117,10 @@ textarea { sans-serif; } +textarea { + min-height: 110px; +} + .pointer-events-none { pointer-events: none; } diff --git a/frontend/src/metabase/entities/databases/forms.js b/frontend/src/metabase/entities/databases/forms.js index 3942d48be0b528e2364f8370dc63aa6a4eb4bc9a..f3cc2951dff46d613fde0229dd2f68ba7ee58c0e 100644 --- a/frontend/src/metabase/entities/databases/forms.js +++ b/frontend/src/metabase/entities/databases/forms.js @@ -45,6 +45,21 @@ const DATABASE_DETAIL_OVERRIDES = { return null; }, }), + "tunnel-private-key": (engine, details) => ({ + title: t`SSH private key`, + placeholder: t`Paste the contents of your ssh private key here`, + type: "text", + }), + "tunnel-private-key-passphrase": (engine, details) => ({ + title: t`Passphrase for the SSH private key`, + }), + "tunnel-auth-option": (engine, details) => ({ + title: t`SSH Authentication`, + options: [ + { name: t`SSH Key`, value: "ssh-key" }, + { name: t`Password`, value: "password" }, + ], + }), }; const AUTH_URL_PREFIXES = { @@ -167,6 +182,24 @@ function getFieldsForEngine(engine, details) { ) { continue; } + + // hide the auth settings based on which auth method is selected + // private key auth needs tunnel-private-key and tunnel-private-key-passphrase + if ( + field.name.startsWith("tunnel-private-") && + details["tunnel-auth-option"] !== "ssh-key" + ) { + continue; + } + + // username / password auth uses tunnel-pass + if ( + field.name === "tunnel-pass" && + details["tunnel-auth-option"] === "ssh-key" + ) { + continue; + } + const overrides = DATABASE_DETAIL_OVERRIDES[field.name]; // convert database details-fields to Form fields fields.push({ @@ -174,6 +207,7 @@ function getFieldsForEngine(engine, details) { title: field["display-name"], type: field.type, placeholder: field.placeholder || field.default, + options: field.options, validate: value => (field.required && !value ? t`required` : null), normalize: value => value === "" || value == null @@ -182,6 +216,7 @@ function getFieldsForEngine(engine, details) { : null : value, horizontal: field.type === "boolean", + initial: field.default, ...(overrides && overrides(engine, details)), }); } diff --git a/modules/drivers/druid/test/metabase/driver/druid/client_test.clj b/modules/drivers/druid/test/metabase/driver/druid/client_test.clj index b252e345c9a97682f2660f63379e0ef2a557df6b..3289525d07af65d749f1621a5bc281e8deec4f52 100644 --- a/modules/drivers/druid/test/metabase/driver/druid/client_test.clj +++ b/modules/drivers/druid/test/metabase/driver/druid/client_test.clj @@ -31,26 +31,30 @@ (mt/wait-for-result cancel-chan 2000))))))))) (deftest ssh-tunnel-test - (mt/test-driver :druid - (let [engine :druid - details {:ssl false - :password "changeme" - :tunnel-host "localhost" - :tunnel-pass "BOGUS-BOGUS" - :port 5432 - :dbname "test" - :host "http://localhost" - :tunnel-enabled true - :tunnel-port 22 - :tunnel-user "bogus"}] - (is (thrown? - com.jcraft.jsch.JSchException - (try - (tu.log/suppress-output - (driver.u/can-connect-with-details? engine details :throw-exceptions)) - (catch Throwable e - (loop [^Throwable e e] - (or (when (instance? com.jcraft.jsch.JSchException e) - (throw e) - e) - (some-> (ex-cause e) recur)))))))))) + (mt/test-driver + :druid + (is (thrown? + java.net.ConnectException + (try + (let [engine :druid + details {:ssl false + :password "changeme" + :tunnel-host "localhost" + :tunnel-pass "BOGUS-BOGUS" + :port 5432 + :dbname "test" + :host "http://localhost" + :tunnel-enabled true + ;; we want to use a bogus port here on purpose - + ;; so that locally, it gets a ConnectionRefused, + ;; and in CI it does too. Apache's SSHD library + ;; doesn't wrap every exception in an SshdException + :tunnel-port 21212 + :tunnel-user "bogus"}] + (tu.log/suppress-output + (driver.u/can-connect-with-details? engine details :throw-exceptions))) + (catch Throwable e + (loop [^Throwable e e] + (or (when (instance? java.net.ConnectException e) + (throw e)) + (some-> (.getCause e) recur))))))))) diff --git a/modules/drivers/mongo/src/metabase/driver/mongo.clj b/modules/drivers/mongo/src/metabase/driver/mongo.clj index e1abacd7973446f0a96774e89821baea14451f80..ff317912a1d7db1d64d8bac016e453f1a54551bd 100644 --- a/modules/drivers/mongo/src/metabase/driver/mongo.clj +++ b/modules/drivers/mongo/src/metabase/driver/mongo.clj @@ -71,10 +71,10 @@ #"^Password can not be null when the authentication mechanism is unspecified$" (driver.common/connection-error-messages :password-required) - #"^com.jcraft.jsch.JSchException: Auth fail$" + #"^org.apache.sshd.common.SshException: No more authentication methods available$" (driver.common/connection-error-messages :ssh-tunnel-auth-fail) - #".*JSchException: java.net.ConnectException: Connection refused.*" + #"^java.net.ConnectException: Connection refused$" (driver.common/connection-error-messages :ssh-tunnel-connection-fail) #".*" ; default diff --git a/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj b/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj index 99d2e0f490c013a81188efc3141b293af9362986..d29a64406ff75c51702a900b947dee40b656aea0 100644 --- a/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj +++ b/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj @@ -1,7 +1,9 @@ (ns metabase.driver.mongo.util-test - (:require [expectations :refer [expect]] + (:require [clojure.test :refer :all] + [expectations :refer [expect]] [metabase.driver.mongo.util :as mongo-util] [metabase.driver.util :as driver.u] + [metabase.test :as mt] [metabase.test.util.log :as tu.log]) (:import [com.mongodb DB MongoClient MongoClientException ReadPreference ServerAddress])) @@ -213,24 +215,32 @@ (-> (#'mongo-util/connection-options-builder :additional-options "readPreference=ternary") .build)) -(expect - com.jcraft.jsch.JSchException - (try - (let [engine :mongo - details {:ssl false - :password "changeme" - :tunnel-host "localhost" - :tunnel-pass "BOGUS-BOGUS" - :port 5432 - :dbname "test" - :host "localhost" - :tunnel-enabled true - :tunnel-port 22 - :tunnel-user "bogus"}] - (tu.log/suppress-output - (driver.u/can-connect-with-details? engine details :throw-exceptions))) - (catch Throwable e - (loop [^Throwable e e] - (or (when (instance? com.jcraft.jsch.JSchException e) - e) - (some-> (.getCause e) recur)))))) +(deftest test-ssh-connection + (testing "Gets an error when it can't connect to mongo via ssh tunnel" + (mt/test-driver + :mongo + (is (thrown? + java.net.ConnectException + (try + (let [engine :mongo + details {:ssl false + :password "changeme" + :tunnel-host "localhost" + :tunnel-pass "BOGUS-BOGUS" + :port 5432 + :dbname "test" + :host "localhost" + :tunnel-enabled true + ;; we want to use a bogus port here on purpose - + ;; so that locally, it gets a ConnectionRefused, + ;; and in CI it does too. Apache's SSHD library + ;; doesn't wrap every exception in an SshdException + :tunnel-port 21212 + :tunnel-user "bogus"}] + (tu.log/suppress-output + (driver.u/can-connect-with-details? engine details :throw-exceptions))) + (catch Throwable e + (loop [^Throwable e e] + (or (when (instance? java.net.ConnectException e) + (throw e)) + (some-> (.getCause e) recur)))))))))) diff --git a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj index db4957ee865952edd3b24ffd0a32529ddc3818ce..4fdd69b9de9ca962b4f26fab2d96fb5b77a9fb1d 100644 --- a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj +++ b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj @@ -8,9 +8,11 @@ [driver :as driver] [query-processor :as qp] [query-processor-test :as qp.test] + [test :as mt] [util :as u]] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] [metabase.driver.sql.query-processor :as sql.qp] + [metabase.driver.util :as driver.u] [metabase.models [field :refer [Field]] [table :refer [Table]]] @@ -68,23 +70,35 @@ (catch Throwable e (driver/humanize-connection-error-message :oracle (.getMessage e))))) -(expect - com.jcraft.jsch.JSchException - (let [engine :oracle - details {:ssl false - :password "changeme" - :tunnel-host "localhost" - :tunnel-pass "BOGUS-BOGUS-BOGUS" - :port 12345 - :service-name "test" - :sid "asdf" - :host "localhost" - :tunnel-enabled true - :tunnel-port 22 - :user "postgres" - :tunnel-user "example"}] - (tu.log/suppress-output - (driver/can-connect? :oracle details)))) +(deftest test-ssh-connection + (testing "Gets an error when it can't connect to oracle via ssh tunnel" + (mt/test-driver + :oracle + (is (thrown? + java.net.ConnectException + (try + (let [engine :oracle + details {:ssl false + :password "changeme" + :tunnel-host "localhost" + :tunnel-pass "BOGUS-BOGUS" + :port 5432 + :dbname "test" + :host "localhost" + :tunnel-enabled true + ;; we want to use a bogus port here on purpose - + ;; so that locally, it gets a ConnectionRefused, + ;; and in CI it does too. Apache's SSHD library + ;; doesn't wrap every exception in an SshdException + :tunnel-port 21212 + :tunnel-user "bogus"}] + (tu.log/suppress-output + (driver.u/can-connect-with-details? engine details :throw-exceptions))) + (catch Throwable e + (loop [^Throwable e e] + (or (when (instance? java.net.ConnectException e) + (throw e)) + (some-> (.getCause e) recur)))))))))) (expect-with-driver :oracle "UTC" diff --git a/modules/drivers/presto/test/metabase/driver/presto_test.clj b/modules/drivers/presto/test/metabase/driver/presto_test.clj index f2c79585e22c4981ae474251a8502fccaaa00826..affa2eed78a8399bc4c4a37aa8ddda8096862ce4 100644 --- a/modules/drivers/presto/test/metabase/driver/presto_test.clj +++ b/modules/drivers/presto/test/metabase/driver/presto_test.clj @@ -139,23 +139,35 @@ {:page {:page 2 :items 5}})) -(expect - "Hmm, we couldn't connect to the database. Make sure your host and port settings are correct" - (try - (let [details {:ssl false - :password "changeme" - :tunnel-host "localhost" - :tunnel-pass "BOGUS-BOGUS" - :catalog "BOGUS" - :host "localhost" - :port 9999 - :tunnel-enabled true - :tunnel-port 22 - :tunnel-user "bogus"}] - (tu.log/suppress-output - (driver.u/can-connect-with-details? :presto details :throw-exceptions))) - (catch Exception e - (.getMessage e)))) +(deftest test-connect-via-tunnel + (testing "connection fails as expected" + (mt/test-driver + :presto + (is (thrown? + java.net.ConnectException + (try + (let [engine :presto + details {:ssl false + :password "changeme" + :tunnel-host "localhost" + :tunnel-pass "BOGUS-BOGUS" + :catalog "BOGUS" + :host "localhost" + :port 9999 + :tunnel-enabled true + ;; we want to use a bogus port here on purpose - + ;; so that locally, it gets a ConnectionRefused, + ;; and in CI it does too. Apache's SSHD library + ;; doesn't wrap every exception in an SshdException + :tunnel-port 21212 + :tunnel-user "bogus"}] + (tu.log/suppress-output + (driver.u/can-connect-with-details? engine details :throw-exceptions))) + (catch Throwable e + (loop [^Throwable e e] + (or (when (instance? java.net.ConnectException e) + (throw e)) + (some-> (.getCause e) recur)))))))))) (deftest db-default-timezone-test (mt/test-driver :presto diff --git a/project.clj b/project.clj index 9c57dcc979dced30b157839294e6faf20488d7cc..837191cd1efacfb1ce8cfabf71d52bf7ae04312d 100644 --- a/project.clj +++ b/project.clj @@ -77,7 +77,6 @@ :exclusions [org.slf4j/slf4j-api it.unimi.dsi/fastutil]] [com.draines/postal "2.0.3"] ; SMTP library - [com.jcraft/jsch "0.1.55"] ; SSH client for tunnels [com.google.guava/guava "28.2-jre"] ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697 [com.h2database/h2 "1.4.197"] ; embedded SQL database [com.mattbertolini/liquibase-slf4j "2.0.0"] ; Java Migrations lib logging. We don't actually use this AFAIK (?) @@ -112,6 +111,7 @@ [metabase/throttle "1.0.2"] ; Tools for throttling access to API endpoints and other code pathways [net.sf.cssbox/cssbox "4.12" :exclusions [org.slf4j/slf4j-api]] ; HTML / CSS rendering [org.apache.commons/commons-lang3 "3.10"] ; helper methods for working with java.lang stuff + [org.apache.sshd/sshd-core "2.4.0"] ; ssh tunneling and test server [org.bouncycastle/bcprov-jdk15on "1.65"] ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors [org.clojars.pntblnk/clj-ldap "0.0.16"] ; LDAP client [org.eclipse.jetty/jetty-server "9.4.27.v20200227"] ; We require JDK 8 which allows us to run Jetty 9.4, ring-jetty-adapter runs on 1.7 which forces an older version @@ -120,9 +120,10 @@ :exclusions [ch.qos.logback/logback-classic]] [org.mariadb.jdbc/mariadb-java-client "2.5.1"] ; MySQL/MariaDB driver [org.postgresql/postgresql "42.2.8"] ; Postgres driver - [org.slf4j/slf4j-log4j12 "1.7.25"] ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time + [org.slf4j/slf4j-api "1.7.30"] ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time + [org.slf4j/slf4j-log4j12 "1.7.30"] ; ^^ [org.tcrawley/dynapath "1.1.0"] ; Dynamically add Jars (e.g. Oracle or Vertica) to classpath - [org.threeten/threeten-extra "1.5.0"] ; extra Java 8 java.time classes like DayOfMonth and Quarter + [org.threeten/threeten-extra "1.5.0"] ; extra Java 8 java.time classes like DayOfMonth and Quarter [org.yaml/snakeyaml "1.23"] ; YAML parser (required by liquibase) [potemkin "0.4.5" :exclusions [riddley]] ; utility macros & fns [pretty "1.0.4"] ; protocol for defining how custom types should be pretty printed diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index a1dfbe42748bf2d9f847791d2eddf150997f75ab..c35f4bb69b280093c8471372aeac66e82c0b3a13 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -283,7 +283,8 @@ :display-name su/NonBlankString ;; Type of this property. Defaults to `:string` if unspecified. - (s/optional-key :type) (s/enum :string :integer :boolean :password) + ;; `:select` is a `String` in the backend. + (s/optional-key :type) (s/enum :string :integer :boolean :password :select :text) ;; A default value for this field if the user hasn't set an explicit value. This is shown in the UI as a ;; placeholder. @@ -295,7 +296,10 @@ (s/optional-key :placeholder) s/Any ;; Is this property required? Defaults to `false`. - (s/optional-key :required?) s/Bool} + (s/optional-key :required?) s/Bool + + ;; Any options for `:select` types + (s/optional-key :options) {s/Keyword s/Str}} (complement (every-pred #(contains? % :default) #(contains? % :placeholder))) "connection details that does not have both default and placeholder")) diff --git a/src/metabase/driver/common.clj b/src/metabase/driver/common.clj index 4b7e6fde661a02b7d910b32b5a2674c28e836d5b..200f735bba49d5b1306205cf55f68225d60049bc 100644 --- a/src/metabase/driver/common.clj +++ b/src/metabase/driver/common.clj @@ -59,11 +59,11 @@ "Map of the db host details field, useful for `connection-properties` implementations" {:name "host" :display-name (deferred-tru "Host") - :default "localhost"}) + :placeholder "localhost"}) (def default-port-details "Map of the db port details field, useful for `connection-properties` implementations. Implementations should assoc a - `:default` key." + `:placeholder` key." {:name "port" :display-name (deferred-tru "Port") :type :integer}) diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj index 9f3fed287230f92187db73bf1f130ff60c850d7d..cef2f15577b55ef20e4d1c319bad1e5a7abd676b 100644 --- a/src/metabase/driver/mysql.clj +++ b/src/metabase/driver/mysql.clj @@ -83,7 +83,7 @@ [_] (ssh/with-tunnel-config [driver.common/default-host-details - (assoc driver.common/default-port-details :default 3306) + (assoc driver.common/default-port-details :placeholder 3306) driver.common/default-dbname-details driver.common/default-user-details driver.common/default-password-details diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj index d24ab97ee541feb883ae8b6701ef5e4b0cf7edb8..0214c3c4a1f0d01825e250118cd405a4edaefbae 100644 --- a/src/metabase/driver/postgres.clj +++ b/src/metabase/driver/postgres.clj @@ -86,7 +86,7 @@ [_] (ssh/with-tunnel-config [driver.common/default-host-details - (assoc driver.common/default-port-details :default 5432) + (assoc driver.common/default-port-details :placeholder 5432) driver.common/default-dbname-details driver.common/default-user-details driver.common/default-password-details diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj index cf5cb4308c27234098c6d78c713f29c3e6f2cb29..1a16452b1d59bac84100e8cafe9bb4c82f9a8f48 100644 --- a/src/metabase/driver/sql_jdbc/connection.clj +++ b/src/metabase/driver/sql_jdbc/connection.clj @@ -87,14 +87,12 @@ (let [details-with-tunnel (ssh/include-ssh-tunnel details) ;; If the tunnel is disabled this returned unchanged spec (connection-details->spec driver details-with-tunnel) properties (data-warehouse-connection-pool-properties driver)] - (assoc (connection-pool/connection-pool-spec spec properties) - :ssh-tunnel (:tunnel-connection details-with-tunnel)))) + (connection-pool/connection-pool-spec spec properties))) -(defn- destroy-pool! [database-id {:keys [ssh-tunnel], :as pool-spec}] +(defn- destroy-pool! [database-id pool-spec] (log/debug (u/format-color 'red (trs "Closing old connection pool for database {0} ..." database-id))) (connection-pool/destroy-connection-pool! pool-spec) - (when ssh-tunnel - (.disconnect ^com.jcraft.jsch.Session ssh-tunnel))) + (ssh/close-tunnel! pool-spec)) (defonce ^:private ^{:doc "A map of our currently open connection pools, keyed by Database `:id`."} database-id->connection-pool diff --git a/src/metabase/util/ssh.clj b/src/metabase/util/ssh.clj index 48cd586589233f44772e34f4bc2e0891065529ff..491177a65bfda7e5d549a81884ef250c90f3a43d 100644 --- a/src/metabase/util/ssh.clj +++ b/src/metabase/util/ssh.clj @@ -1,36 +1,66 @@ (ns metabase.util.ssh - (:require [clojure.string :as str] - [clojure.tools.logging :as log] + (:require [clojure.tools.logging :as log] [metabase.util :as u]) - (:import com.jcraft.jsch.JSch)) + (:import java.io.ByteArrayInputStream + org.apache.sshd.client.future.ConnectFuture + org.apache.sshd.client.session.ClientSession + org.apache.sshd.client.session.forward.PortForwardingTracker + org.apache.sshd.client.SshClient + [org.apache.sshd.common.config.keys FilePasswordProvider FilePasswordProvider$ResourceDecodeResult] + org.apache.sshd.common.session.SessionHolder + org.apache.sshd.common.util.GenericUtils + org.apache.sshd.common.util.io.resource.AbstractIoResource + org.apache.sshd.common.util.net.SshdSocketAddress + org.apache.sshd.common.util.security.SecurityUtils + org.apache.sshd.server.forward.AcceptAllForwardingFilter)) (def ^:private default-ssh-timeout 30000) -(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 .disconnect - on the returned connection object." - [{:keys [tunnel-host tunnel-port tunnel-user tunnel-pass host port]}] - (let [connection (doto ^com.jcraft.jsch.Session (.getSession (new com.jcraft.jsch.JSch) - ^String tunnel-user - ^String tunnel-host - tunnel-port) - (.setPassword ^String tunnel-pass) - (.setConfig "StrictHostKeyChecking" "no") - (.connect default-ssh-timeout) - (.setPortForwardingL 0 host port)) - input-port (some-> (.getPortForwardingL connection) - first - (str/split #":") - first - (Integer/parseInt))] - (assert (number? input-port)) - (log/info (u/format-color 'cyan "creating ssh tunnel %s@%s:%s -L %s:%s:%s" tunnel-user tunnel-host tunnel-port input-port host port)) - [connection input-port])) +(def ^:private ^SshClient client + (doto (SshClient/setUpDefaultClient) + (.start) + (.setForwardingFilter AcceptAllForwardingFilter/INSTANCE))) + +(defn- maybe-add-tunnel-password! + [^ClientSession session ^String tunnel-pass] + (when tunnel-pass + (.addPasswordIdentity session tunnel-pass))) + +(defn- maybe-add-tunnel-private-key! + [^ClientSession session ^String tunnel-private-key tunnel-private-key-passphrase] + (when tunnel-private-key + (let [resource-key (proxy [AbstractIoResource] [(class "key") "key"]) + password-provider (proxy [FilePasswordProvider] [] + (getPassword [_ _ _] + tunnel-private-key-passphrase) + (handleDecodeAttemptResult [_ _ _ _ _] + FilePasswordProvider$ResourceDecodeResult/TERMINATE)) + ids (with-open [is (ByteArrayInputStream. (.getBytes tunnel-private-key "UTF-8"))] + (SecurityUtils/loadKeyPairIdentities session resource-key is password-provider)) + keypair (GenericUtils/head ids)] + (.addPublicKeyIdentity session keypair)))) + +(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." + [{:keys [^String tunnel-host ^Integer tunnel-port ^String tunnel-user tunnel-pass tunnel-private-key + tunnel-private-key-passphrase host port]}] + (let [^ConnectFuture conn-future (.connect client tunnel-user tunnel-host tunnel-port) + ^SessionHolder conn-status (.verify conn-future default-ssh-timeout) + session (doto ^ClientSession (.getSession conn-status) + (maybe-add-tunnel-password! tunnel-pass) + (maybe-add-tunnel-private-key! tunnel-private-key tunnel-private-key-passphrase) + (.. auth (verify default-ssh-timeout))) + tracker (.createLocalPortForwardingTracker session + (SshdSocketAddress. "" 0) + (SshdSocketAddress. host port)) + input-port (.. tracker getBoundAddress getPort)] + (log/trace (u/format-color 'cyan "creating ssh tunnel %s@%s:%s -L %s:%s:%s" tunnel-user tunnel-host tunnel-port input-port host port)) + [session tracker])) (def ssh-tunnel-preferences "Configuration parameters to include in the add driver page on drivers that - support ssh tunnels" + support ssh tunnels" [{:name "tunnel-enabled" :display-name "Use SSH tunnel" :placeholder "Enable this ssh tunnel?" @@ -49,19 +79,26 @@ :display-name "SSH tunnel username" :placeholder "What username do you use to login to the SSH tunnel?" :required true} + ;; this is entirely a UI flag + {:name "tunnel-auth-option" + :display-name "SSH Authentication" + :type :select + :options [{:name "SSH Key" :value "ssh-key"} + {:name "Password" :value "password"}] + :default "ssh-key"} {:name "tunnel-pass" :display-name "SSH tunnel password" :type :password - :placeholder "******" - :required true} - #_{:name "tunnel-private-key" + :placeholder "******"} + {:name "tunnel-private-key" :display-name "SSH private key to connect to the tunnel" :type :string - :placeholder "Paste the contents of an ssh private key here"} - #_{:name "tunnel-private-key-file-name" - :display-name "Path on the Metabase server to a SSH private key file to connect to the tunnel" - :type :string - :placeholder "/home/YOUR-USERNAME/.ssh/id_rsa"}]) + :placeholder "Paste the contents of an ssh private key here" + :required true} + {:name "tunnel-private-key-passphrase" + :display-name "Passphrase for SSH private key" + :type :password + :placeholder "******"}]) (defn with-tunnel-config "Add preferences for ssh tunnels to a drivers :connection-properties" @@ -75,34 +112,42 @@ (defn include-ssh-tunnel "Updates connection details for a data warehouse to use the ssh tunnel host and port - For drivers that enter hosts including the protocol (https://host), copy the protocol over as well" + For drivers that enter hosts including the protocol (https://host), copy the protocol over as well" [details] (if (use-ssh-tunnel? details) - (let [[_ proto host] (re-find #"(.*://)?(.*)" (:host details)) - [connection tunnel-entrance-port] (start-ssh-tunnel (assoc details :host host)) ;; don't include L7 protocol in ssh tunnel - details-with-tunnel (assoc details - :port tunnel-entrance-port ;; This parameter is set dynamically when the connection is established - :host (str proto "localhost") - :tunnel-entrance-port tunnel-entrance-port ;; the input port is not known until the connection is opened - :tunnel-connection connection)] + (let [[_ proto host] (re-find #"(.*://)?(.*)" (:host details)) + [session ^PortForwardingTracker tracker] (start-ssh-tunnel! (assoc details :host host)) + tunnel-entrance-port (.. tracker getBoundAddress getPort) + tunnel-entrance-host (.. tracker getBoundAddress getHostName) + details-with-tunnel (assoc details + :port tunnel-entrance-port ;; This parameter is set dynamically when the connection is established + :host (str proto "localhost") ;; SSH tunnel will always be through localhost + :tunnel-entrance-host tunnel-entrance-host + :tunnel-entrance-port tunnel-entrance-port ;; the input port is not known until the connection is opened + :tunnel-session session + :tunnel-tracker tracker)] details-with-tunnel) details)) +(defn close-tunnel! + "Close a running tunnel session" + [details] + (when (use-ssh-tunnel? details) + (.close ^ClientSession (:tunnel-session details)))) + (defn with-ssh-tunnel* "Starts an SSH tunnel, runs the supplied function with the tunnel open, then closes it" - [{:keys [host port tunnel-host tunnel-user tunnel-pass] :as details} f] + [details f] (if (use-ssh-tunnel? details) (let [details-with-tunnel (include-ssh-tunnel details)] - (log/errorf "\nbefore:\n%s\n" (with-out-str (clojure.pprint/pprint details))) - (log/errorf "\nafter:\n%s\n" (with-out-str (clojure.pprint/pprint details-with-tunnel))) (try - (log/info (u/format-color 'cyan "<< OPENED SSH TUNNEL >>")) + (log/trace (u/format-color 'cyan "<< OPENED SSH TUNNEL >>")) (f details-with-tunnel) (catch Exception e (throw e)) (finally - (.disconnect ^com.jcraft.jsch.Session (:tunnel-connection details-with-tunnel)) - (log/info (u/format-color 'cyan "<< CLOSED SSH TUNNEL >>"))))) + (close-tunnel! details-with-tunnel) + (log/trace (u/format-color 'cyan "<< CLOSED SSH TUNNEL >>"))))) (f details))) (defmacro with-ssh-tunnel diff --git a/test/metabase/driver/sql_jdbc_test.clj b/test/metabase/driver/sql_jdbc_test.clj index 54612c80e2c769da635ff89f7799e34d0698da6e..3f45e1a2fee696db477077e40883967fb1d1383e 100644 --- a/test/metabase/driver/sql_jdbc_test.clj +++ b/test/metabase/driver/sql_jdbc_test.clj @@ -81,9 +81,9 @@ (mt/test-driver :postgres (testing "Make sure invalid ssh credentials are detected if a direct connection is possible" (is (thrown? - com.jcraft.jsch.JSchException + java.net.ConnectException + ;; this test works if sshd is running or not (try - ;; this test works if sshd is running or not (let [details {:dbname "test" :engine :postgres :host "localhost" @@ -93,18 +93,21 @@ :tunnel-enabled true :tunnel-host "localhost" ; this test works if sshd is running or not :tunnel-pass "BOGUS-BOGUS-BOGUS" - :tunnel-port 22 + ;; we want to use a bogus port here on purpose - + ;; so that locally, it gets a ConnectionRefused, + ;; and in CI it does too. Apache's SSHD library + ;; doesn't wrap every exception in an SshdException + :tunnel-port 21212 :tunnel-user "example" :user "postgres"}] (tu.log/suppress-output - (driver.u/can-connect-with-details? :postgres details :throw-exceptions))) + (driver.u/can-connect-with-details? :postgres details :throw-exceptions))) (catch Throwable e (loop [^Throwable e e] - (or (when (instance? com.jcraft.jsch.JSchException e) + (or (when (instance? java.net.ConnectException e) (throw e)) (some-> (.getCause e) recur)))))))))) - ;;; --------------------------------- Tests for splice-parameters-into-native-query ---------------------------------- (deftest splice-parameters-native-test diff --git a/test/metabase/util/ssh_test.clj b/test/metabase/util/ssh_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..10cecff04d6cea28bc945c0e76fb55e34f1e3dfe --- /dev/null +++ b/test/metabase/util/ssh_test.clj @@ -0,0 +1,162 @@ +(ns metabase.util.ssh-test + (:require [clojure.java.io :as io] + [clojure.test :refer :all] + [clojure.tools.logging :as log] + [metabase.util.ssh :as sshu]) + (:import (java.io BufferedReader InputStreamReader PrintWriter) + (java.net InetSocketAddress ServerSocket Socket) + org.apache.sshd.server.forward.AcceptAllForwardingFilter)) + +(def ^:private ssh-username "jsmith") +(def ^:private ssh-password "supersecret") +(def ^:private ssh-publickey "ssh/ssh_test.pub") +(def ^:private ssh-key "ssh/ssh_test") +(def ^:private ssh-key-invalid "ssh/ssh_test_invalid") +(def ^:private ssh-publickey-passphrase "ssh/ssh_test_passphrase.pub") +(def ^:private ssh-key-with-passphrase "ssh/ssh_test_passphrase") +(def ^:private ssh-key-passphrase "Password1234") +(def ^:private ssh-mock-server-with-password-port 12221) +(def ^:private ssh-mock-server-with-publickey-port 12222) +(def ^:private ssh-mock-server-with-publickey-passphrase-port 12223) + +;;-------------- +;; mock ssh server fixtures +;;-------------- + +(defn start-ssh-mock-server-with-password + "start a ssh mock server with password auth challenge" + [] + (let [password-auth (reify org.apache.sshd.server.auth.password.PasswordAuthenticator + (authenticate [_ username password session] + (and + (= username ssh-username) + (= password ssh-password)))) + keypair-provider (new org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider) + sshd (doto (org.apache.sshd.server.SshServer/setUpDefaultServer) + (.setPort ssh-mock-server-with-password-port) + (.setKeyPairProvider keypair-provider) + (.setPasswordAuthenticator password-auth))] + (log/debug "ssh mock server (with password) started") + (.start sshd) + sshd)) + +(defn start-ssh-mock-server-with-publickey + "start a ssh mock server with public key auth challenge" + [pubkey port] + (let [keypair-provider (new org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider) + publickey-file (io/file (io/resource pubkey)) + publickey-auth (new org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator + (.toPath publickey-file)) + sshd (doto (org.apache.sshd.server.SshServer/setUpDefaultServer) + (.setPort port) + (.setKeyPairProvider keypair-provider) + (.setPublickeyAuthenticator publickey-auth) + (.setForwardingFilter AcceptAllForwardingFilter/INSTANCE))] + (log/debug "ssh mock server (with publickey) started") + (.start sshd) + sshd)) + +(use-fixtures :once + (fn [f] + (let [servers [(start-ssh-mock-server-with-password) + (start-ssh-mock-server-with-publickey ssh-publickey ssh-mock-server-with-publickey-port) + (start-ssh-mock-server-with-publickey ssh-publickey-passphrase ssh-mock-server-with-publickey-passphrase-port)]] + (try (f) + (finally + (doseq [server servers] + (try + (when server + (.stop server)) + (catch Exception e + (log/error e))))))))) + +;;-------------- +;; tests +;;-------------- + +;; correct password +(deftest connects-with-correct-password + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-password-port + :tunnel-pass ssh-password + :host "127.0.0.1" + :port 1234})) + +;; incorrect password +(deftest throws-exception-on-incorrect-password + (is (thrown? org.apache.sshd.common.SshException + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-password-port + :tunnel-pass (str ssh-password "invalid") + :host "127.0.0.1" + :port 1234})))) + +;; correct ssh key +(deftest connects-with-correct-ssh-key + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-publickey-port + :tunnel-private-key (slurp (io/resource ssh-key)) + :host "127.0.0.1" + :port 1234})) + +;; incorrect ssh key +(deftest throws-exception-on-incorrect-ssh-key + (is (thrown? org.apache.sshd.common.SshException + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-publickey-port + :tunnel-private-key (slurp (io/resource ssh-key-invalid)) + :host "127.0.0.1" + :port 1234})))) + +;; correct ssh key +(deftest connects-with-correct-ssh-key-and-passphrase + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-publickey-passphrase-port + :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase)) + :tunnel-private-key-passphrase ssh-key-passphrase + :host "127.0.0.1" + :port 1234})) + +(deftest throws-exception-on-incorrect-ssh-key-and-passphrase + (is (thrown? java.io.StreamCorruptedException + (sshu/start-ssh-tunnel! + {:tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-publickey-passphrase-port + :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase)) + :tunnel-private-key-passphrase "this-is-the-wrong-passphrase" + :host "127.0.0.1" + :port 1234})))) + +(deftest ssh-tunnel-works + (testing "ssh tunnel can properly tunnel" + ;; this will try to open a TCP connection via the tunnel. If it fails, + (sshu/with-ssh-tunnel [details-with-tunnel {:tunnel-enabled true + :tunnel-user ssh-username + :tunnel-host "127.0.0.1" + :tunnel-port ssh-mock-server-with-publickey-passphrase-port + :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase)) + :tunnel-private-key-passphrase ssh-key-passphrase + :host "127.0.0.1" + :port 41414}] + (with-open [server (doto (ServerSocket. 41414) + (.setSoTimeout 10000)) + socket (Socket.)] + (let [server-thread (future (with-open [client-socket (.accept server) + out-server (PrintWriter. (.getOutputStream client-socket) true)] + (.println out-server "hello from the ssh tunnel")))] + (.connect socket (InetSocketAddress. "127.0.0.1" (:tunnel-entrance-port details-with-tunnel)) 3000) + ;; cause our future to run to completion + @server-thread + (with-open [in-client (BufferedReader. (InputStreamReader. (.getInputStream socket)))] + (is (= "hello from the ssh tunnel" (.readLine in-client))))))))) diff --git a/test_resources/ssh/README.md b/test_resources/ssh/README.md new file mode 100644 index 0000000000000000000000000000000000000000..aa5beaabe860f24a63be72c68496254653cb18de --- /dev/null +++ b/test_resources/ssh/README.md @@ -0,0 +1,7 @@ +# ssh test keys + +Use this command to generate these keys: + +``` +$ ssh-keygen -t rsa -m PEM +``` diff --git a/test_resources/ssh/ssh_test b/test_resources/ssh/ssh_test new file mode 100644 index 0000000000000000000000000000000000000000..b7653ef38c4e3fdb7f5c46c41384a7e31a68fac1 --- /dev/null +++ b/test_resources/ssh/ssh_test @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/test_resources/ssh/ssh_test.pub b/test_resources/ssh/ssh_test.pub new file mode 100644 index 0000000000000000000000000000000000000000..a997410bbba2462bc14f673d6ae3ddbae9865c38 --- /dev/null +++ b/test_resources/ssh/ssh_test.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGOIPsjFweWcVxTQfAx6cZc2XN+wLdrd1KDrP1MipwYgWE0V51/U+d8ScDPjUGYltabxKXEOMQaVxlDpedIPVN51glKA5L9IKtNO8om2eX3xNXUJWlHnPtA0GQGSS0EfMWtkdipOX+AikzIRPzik91FsKDsxnZD79ZrtyDo39INEQxJigpcv6xpD8f3n9o1F+QgsQekaMtIZwo43pSaVJZnpaoaDORk9/0FI8Keq9vMxjlvPFdUgC6Ib3CO5CSgAH2Ev9DCNi3q3apLsPxeTHoguYqv86ujA9tMwanWmc6HWDvbwM2g9497s2k3gPT6RzPsgd6f/9ZhZPegeaLF4gOEza5NgLwJ0oKWnZyNWoZEpPU0LS4uXRc+dBVVNF8J569q9uyl2OUkd/ICqx0P+rzx2A/1FUZwxgyry4lcCvLxJCmFFrt4m6/dlhvuyszip6gDfb1K8cAPQADnZoflaDfGmU3XpIetKnwKs47+eQIvDTzr/+HHv24E2HNbzkFYQM= diff --git a/test_resources/ssh/ssh_test_invalid b/test_resources/ssh/ssh_test_invalid new file mode 100644 index 0000000000000000000000000000000000000000..15845157488cdd0889b1bd05d796d2121baf1a58 --- /dev/null +++ b/test_resources/ssh/ssh_test_invalid @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/test_resources/ssh/ssh_test_invalid.pub b/test_resources/ssh/ssh_test_invalid.pub new file mode 100644 index 0000000000000000000000000000000000000000..3a5715bba75da503e806417b28b500d0615ddd4c --- /dev/null +++ b/test_resources/ssh/ssh_test_invalid.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAFKKaiSiicpURjxbaTmk27s7iwZGs2GYRDXeO0BI8jJHha4akh/VN5cP+9r0DwlPsiqnPfGVVzKS9s3SnFxNUxexBAu2tVmYRsEnd418pUb/EhJkpAM9XlgzgWG02HKGXyotDHIJOcloTW1Am9XQv3qbAInhObBfdDjtBMm4LquR0TBbeBilQTrt6csQ7DfQQhOhLZ6V6HNlSb/M66MC89ZPRdV3FwWeodBC8Hp162oQE/pfDyoKtIav2O5WHoalJcpnXZ8HEktQFn1c7nOOYb0BUDfC0+hBMhyORmDL30xEbAo3fs4h3j5bENFTQA5DFrBZ68BX7ie6OqC2foKNJb89okToBA93GDcCjcnOsQRTH7N324P6aaz50vD2sqJS4X0x2XA7XMoZ96EBM2U+jomQ8aiUoDgbiQI5kSwa0nEERJ8aXGNzm3wO5JwnanTj9MM6WfftDsB69SxBH203oY3BF+FKD0xlwj30z+fhpSqGW9ZLWnhlYhWkFQRM1WP8= diff --git a/test_resources/ssh/ssh_test_passphrase b/test_resources/ssh/ssh_test_passphrase new file mode 100644 index 0000000000000000000000000000000000000000..49530610bba07751171a6c0eb96f634f8d16d181 --- /dev/null +++ b/test_resources/ssh/ssh_test_passphrase @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +Wwh5+ivBXeXSoE099eFwO7dMERI= +-----END OPENSSH PRIVATE KEY----- diff --git a/test_resources/ssh/ssh_test_passphrase.pub b/test_resources/ssh/ssh_test_passphrase.pub new file mode 100644 index 0000000000000000000000000000000000000000..6ab3b3bd2c357713f8851474db24c045d17ec63f --- /dev/null +++ b/test_resources/ssh/ssh_test_passphrase.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrB4H3qCaYrNt1P+mDrF23+NJ5M6povEzFAM/TcFm0kT0mMhqoBzFGu0ELUbew1xdoH0kbkpo2b7ur4M+tjC96196Z4Soa1N7M4VNokrgmb0f0iMV/rsPeEIEJgPQBn4YAepWtLUCeaZQ5TOzImwUjVeuvMzAzRDowJPDyM1POiY/JibMI/UCiZZny5W5qLTvhJSiyTNuiC+RjdSkFN2heEJyE0r4LN4Psk5wUXxdWDgIw5H978y7MDZNdPzTl8t+X8nARYLoXnE1ne4lHXBsZg06UUPf/IhPmoxp4CI1j8W+jNbDg0+3ceLQ2etVa4Vjj68cE8giaXfFMcqpeIh0M8SegomR89lNGwsPBdnQMrNi5Oxg5avAQo41Rq/MkK8YKDjbF4I+UpKCr8W280EYhC+b+tftynryMrZ2WDcTmoCyAhh9ltb7ab7GCxKgBs3t5+l9b/8GJf1vPBtYb45zVjy71+ppAy/sFt/NoaLx9CV6KKEoQnw2dWmYi0P9lfcU= rroland@carter