From 2d93c5aeebf31a3f28b9bb3321413ba9a78b34ef Mon Sep 17 00:00:00 2001 From: Robert Roland <rob@metabase.com> Date: Tue, 26 May 2020 09:47:19 -0700 Subject: [PATCH] ssh key support (#12548) Adding SSH key support Adding ssh key support (with passphrases) The JSch library does not support ED25519 keys, that are the default as of OpenSSH 7.8, but the Apache Mina SSHD library does. Adds a end-to-end test of ssh port forwarding via a simple echo server. [ci drivers] Co-authored-by: Franklin Strube <thedoc8786@gmail.com> Co-authored-by: hbc <bcxxxxxx@gmail.com> Co-authored-by: Daniel Higginbotham <daniel@flyingmachinestudios.com> Co-authored-by: Paul Rosenzweig <paul.a.rosenzweig@gmail.com> Co-authored-by: Kyle Doherty <kyle.l.doherty@gmail.com> --- frontend/src/metabase/containers/Form.jsx | 49 ++++-- frontend/src/metabase/css/core/base.css | 4 + .../src/metabase/entities/databases/forms.js | 35 ++++ .../metabase/driver/druid/client_test.clj | 50 +++--- .../mongo/src/metabase/driver/mongo.clj | 4 +- .../test/metabase/driver/mongo/util_test.clj | 54 +++--- .../test/metabase/driver/oracle_test.clj | 48 ++++-- .../test/metabase/driver/presto_test.clj | 46 +++-- project.clj | 7 +- src/metabase/driver.clj | 8 +- src/metabase/driver/common.clj | 4 +- src/metabase/driver/mysql.clj | 2 +- src/metabase/driver/postgres.clj | 2 +- src/metabase/driver/sql_jdbc/connection.clj | 8 +- src/metabase/util/ssh.clj | 139 ++++++++++----- test/metabase/driver/sql_jdbc_test.clj | 15 +- test/metabase/util/ssh_test.clj | 162 ++++++++++++++++++ test_resources/ssh/README.md | 7 + test_resources/ssh/ssh_test | 39 +++++ test_resources/ssh/ssh_test.pub | 1 + test_resources/ssh/ssh_test_invalid | 39 +++++ test_resources/ssh/ssh_test_invalid.pub | 1 + test_resources/ssh/ssh_test_passphrase | 39 +++++ test_resources/ssh/ssh_test_passphrase.pub | 1 + 24 files changed, 600 insertions(+), 164 deletions(-) create mode 100644 test/metabase/util/ssh_test.clj create mode 100644 test_resources/ssh/README.md create mode 100644 test_resources/ssh/ssh_test create mode 100644 test_resources/ssh/ssh_test.pub create mode 100644 test_resources/ssh/ssh_test_invalid create mode 100644 test_resources/ssh/ssh_test_invalid.pub create mode 100644 test_resources/ssh/ssh_test_passphrase create mode 100644 test_resources/ssh/ssh_test_passphrase.pub diff --git a/frontend/src/metabase/containers/Form.jsx b/frontend/src/metabase/containers/Form.jsx index 2784d1299ec..4a12d1a425c 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 589676e79f9..2526b7f14dc 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 3942d48be0b..f3cc2951dff 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 b252e345c9a..3289525d07a 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 e1abacd7973..ff317912a1d 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 99d2e0f490c..d29a64406ff 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 db4957ee865..4fdd69b9de9 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 f2c79585e22..affa2eed78a 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 9c57dcc979d..837191cd1ef 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 a1dfbe42748..c35f4bb69b2 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 4b7e6fde661..200f735bba4 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 9f3fed28723..cef2f15577b 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 d24ab97ee54..0214c3c4a1f 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 cf5cb4308c2..1a16452b1d5 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 48cd5865892..491177a65bf 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 54612c80e2c..3f45e1a2fee 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 00000000000..10cecff04d6 --- /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 00000000000..aa5beaabe86 --- /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 00000000000..b7653ef38c4 --- /dev/null +++ b/test_resources/ssh/ssh_test @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG5gIBAAKCAYEAxjiD7IxcHlnFcU0HwMenGXNlzfsC3a3dSg6z9TIqcGIFhNFe +df1PnfEnAz41BmJbWm8SlxDjEGlcZQ6XnSD1TedYJSgOS/SCrTTvKJtnl98TV1CV +pR5z7QNBkBkktBHzFrZHYqTl/gIpMyET84pPdRbCg7MZ2Q+/Wa7cg6N/SDREMSYo +KXL+saQ/H95/aNRfkILEHpGjLSGcKON6UmlSWZ6WqGgzkZPf9BSPCnqvbzMY5bzx +XVIAuiG9wjuQkoAB9hL/QwjYt6t2qS7D8Xkx6ILmKr/OrowPbTMGp1pnOh1g728D +NoPePe7NpN4D0+kcz7IHen//WYWT3oHmixeIDhM2uTYC8CdKClp2cjVqGRKT1NC0 +uLl0XPnQVVTRfCeevavbspdjlJHfyAqsdD/q88dgP9RVGcMYMq8uJXAry8SQphRa +7eJuv3ZYb7srM4qeoA329SvHAD0AA52aH5Wg3xplN16SHrSp8CrOO/nkCLw086// +hx79uBNhzW85BWEDAgMBAAECggGBALtRNPoZOgREeV00mhsHkVVvw8j/aBns763I +by9LFOfW+bgl0spVcyOifGeIJbu+vu2bAUpY3vrnVjT5sTT/rFDOSnHyhHAqxELC +Py90jFTsre5ZbND5EjvsU8zEtfak77+KybLieaWsjRqQK7Z+AdB4jaC/Y7HIO9+Y +azLLEsE9AyQfFtz0mtNsj2qibGy4JQb/TC5HGpjPpi7NqQ6wEO43+89xiSoNQBn8 +Q6B3h8nh06lnAABcOeurbxNaDqki5QqOxdCzmPq7fC2HSgx2RMuaKe10fovr+odr +GBcrEZV10vMcaV5zb2/P4zk391kRSnPZ0Pc/ixTGxEX+gX1Kboxalskbb0TH11Tg +LGGrAsk7Unj71p4AA11fgoPm7nljz4xXNiwB3rZZdUBzUZEQbVwpkOdFZkwL/XRC +klGAekLiize47KV98RgAnn0U8T9BaKZNU0e9Xe7bYzORUp0KdW8ymehSQd5HJ0AH +/SYoimhMNGoCmuL34onCUOPhrM0CwQKBwQD+5ySvl0rR/LnAr5bq5gWTeL6J1Kxk +wyg05HboLFj3uWgQqgqge+pr1q2Gb8x4thtHIUzQ5mDX4aOmep00msSPNjMURPKG +rdsb0U6rGVb4ndukv9ccytOwMtmz4kcMWGuMPIIj+upnuSnf0J2ixY82jqz6HjMZ +3OHKrwxQjADwc6sRcB6Eb4vFZL1jxgthRgqaLDKxVS5Vb9NwmK9eOrpkfVXd9EcE +LR/pfSxV1vgy/kA21vi+6qpG7Z6RaadLFekCgcEAxxLrKXg6KHZpAlePwLdgxWkr +v0gsF2C+Zjt7pff4EXOYZ8qfr59ueZbQolPkL0PbPEI0Xedz7epnLgOEz/tAXgnC +bzZwuSPeOI/MBYMycRF7aBMMwdnSZ3yb+ci1IHwtdIDIeN5VqJ8Op7lpuNmm3k+c +xAYRykCuiw2UlD9OJLmMzH5jNTTSpkQbhwY+ghUPI2doFQKz7meUP5oH0BDurFC7 +tPb5cE4Th0Sze4h0r4EGOMavsIhMGA6vtwasmfALAoHBALec+Kgjaxnn2kYaNbPv +DYU1LMtMDwJmMcgn6h9EErIfM/8M/aqsmCgl88krLzaktvF94z93M7tOJfv9xs/l +zED84b5wC+NHyNU46FoHXsanr1f1eJac4+/AMWGKVXNnHdFepMAWNlOQ5cD7HRHr +DUZXb/KbXmP64AqIHW7H6sVKDKf7A4CSeTQvZN1CA2CGe4yi2cEzgrS4YK5yzaAq +3akVP40qMR2pA5vFNvJ+bzsMOmVGZNfhYdbFw7srR/6mQQKBwQCctv/5fXQLNmwk +M9ou1C2SOuD9jEtpe/dnc5w7Y6Id2uo3iwN6tf+6KEfGAlS4AKsuHNAsvHA+8zCW +wJ5lPF3HqdcuxaSnmtztmgX6sPWcnS6RF64LTPaeETKYyLAOCrOd62PmAuFcBRr8 +XrIjmvQKPpIinsSSe6jsPpygt9VEg+2bbkObNyI9UZB6EyhSL1HjhRwiriYHn/LM +vu03lpzNeiDKrUJgbpZg/mxs61cwcln7iC32wtVkeutJIi/uX1ECgcEAvpRr/tPU +oQFQGuhDObb63PYpuQVSYf1l4UF5qpQvs/peYCQDr6BFOMxP71eDs7hjP/j+Yyg6 +AHItCz36yb5VfI3L7O9tIPhkrANUnDWk1n9lSA99F0XCWhpzhs5ble8xDQAZ7rGq +Qp0BHKJ70iWqdZAEgA3L1E53w1yXW26uj+LWHygKjuIGVeQy0FsnV7QecxikKiMo +VAj9X0pRheVwjnSHCaVKHc0tW/xRRwPdkZ9GTBCjS8teW78NLOWFMEsv +-----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 00000000000..a997410bbba --- /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 00000000000..15845157488 --- /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 00000000000..3a5715bba75 --- /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 00000000000..49530610bba --- /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 00000000000..6ab3b3bd2c3 --- /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 -- GitLab