diff --git a/frontend/src/metabase/entities/databases/forms.js b/frontend/src/metabase/entities/databases/forms.js index e0584efa8a4fc8c44c86f4ecafd54a912957e665..b060f4c91f92769f40656f426c8aec5c3ececf13 100644 --- a/frontend/src/metabase/entities/databases/forms.js +++ b/frontend/src/metabase/entities/databases/forms.js @@ -10,22 +10,22 @@ import MetadataSyncScheduleWidget from "metabase/admin/databases/components/widg import CacheFieldValuesScheduleWidget from "metabase/admin/databases/components/widgets/CacheFieldValuesScheduleWidget"; const DATABASE_DETAIL_OVERRIDES = { - "tunnel-enabled": (engine, details) => ({ + "tunnel-enabled": (engine, details, id) => ({ title: t`Use an SSH-tunnel for database connections`, description: t`Some database installations can only be accessed by connecting through an SSH bastion host. This option also provides an extra layer of security when a VPN is not available. Enabling this is usually slower than a direct connection.`, }), - "use-jvm-timezone": (engine, details) => ({ + "use-jvm-timezone": (engine, details, id) => ({ title: t`Use the Java Virtual Machine (JVM) timezone`, description: t`We suggest you leave this off unless you're doing manual timezone casting in many or most of your queries with this data.`, }), - "use-srv": (engine, details) => ({ + "use-srv": (engine, details, id) => ({ title: t`Use DNS SRV when connecting`, description: t`Using this option requires that provided host is a FQDN. If connecting to an Atlas cluster, you might need to enable this option. If you don't know what this means, leave this disabled.`, }), - "client-id": (engine, details) => ({ + "client-id": (engine, details, id) => ({ description: getClientIdDescription(engine, details), }), - "auth-code": (engine, details) => ({ + "auth-code": (engine, details, id) => ({ description: ( <div> <div>{getAuthCodeLink(engine, details)}</div> @@ -33,8 +33,13 @@ const DATABASE_DETAIL_OVERRIDES = { </div> ), }), - "service-account-json": (engine, details) => ({ + "service-account-json": (engine, details, id) => ({ validate: value => { + // this field is only required if this is a new entry + if (id) { + return null; + } + if (!value) { return t`required`; } @@ -46,22 +51,22 @@ const DATABASE_DETAIL_OVERRIDES = { return null; }, }), - "tunnel-private-key": (engine, details) => ({ + "tunnel-private-key": (engine, details, id) => ({ title: t`SSH private key`, placeholder: t`Paste the contents of your ssh private key here`, type: "text", }), - "tunnel-private-key-passphrase": (engine, details) => ({ + "tunnel-private-key-passphrase": (engine, details, id) => ({ title: t`Passphrase for the SSH private key`, }), - "tunnel-auth-option": (engine, details) => ({ + "tunnel-auth-option": (engine, details, id) => ({ title: t`SSH Authentication`, options: [ { name: t`SSH Key`, value: "ssh-key" }, { name: t`Password`, value: "password" }, ], }), - "ssl-cert": (engine, details) => ({ + "ssl-cert": (engine, details, id) => ({ title: t`Server SSL certificate chain`, placeholder: t`Paste the contents of the server's SSL certificate chain here`, type: "text", @@ -234,7 +239,7 @@ function getFieldsForEngine(engine, details, id) { horizontal: field.type === "boolean", initial: field.default, readOnly: field.readOnly || false, - ...(overrides && overrides(engine, details)), + ...(overrides && overrides(engine, details, id)), }); } return fields; diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj index 4de66157cc461ebbf2394660a8482d598d9c4f38..97379a3a8bb74d1655f1223f86115c6f23d979c1 100644 --- a/src/metabase/api/database.clj +++ b/src/metabase/api/database.clj @@ -3,6 +3,7 @@ (:require [clojure.string :as str] [clojure.tools.logging :as log] [compojure.core :refer [DELETE GET POST PUT]] + [medley.core :as m] [metabase [config :as config] [driver :as driver] @@ -533,6 +534,19 @@ ;;; --------------------------------------------- PUT /api/database/:id ---------------------------------------------- +(defn upsert-sensitive-fields + "Replace any sensitive values not overriden in the PUT with the original values" + [database details] + (when details + (merge (:details database) + (reduce + (fn [details k] + (if (= protected-password (get details k)) + (m/update-existing details k (constantly (get-in database [:details k]))) + details)) + details + database/sensitive-fields)))) + (api/defendpoint PUT "/:id" "Update a `Database`." [id :as {{:keys [name engine details is_full_sync is_on_demand description caveats points_of_interest schedules @@ -547,9 +561,7 @@ auto_run_queries (s/maybe s/Bool)} (api/check-superuser) (api/let-404 [database (Database id)] - (let [details (if-not (= protected-password (:password details)) - details - (assoc details :password (get-in database [:details :password]))) + (let [details (upsert-sensitive-fields database details) conn-error (when (some? details) (assert (some? engine)) (test-database-connection engine details)) @@ -565,18 +577,18 @@ ;; that seems like the kind of thing that will almost never work in any practical way ;; TODO - this means one cannot unset the description. Does that matter? (api/check-500 (db/update-non-nil-keys! Database id - (merge - {:name name - :engine engine - :details details - :is_full_sync full-sync? - :is_on_demand (boolean is_on_demand) - :description description - :caveats caveats - :points_of_interest points_of_interest - :auto_run_queries auto_run_queries} - (when schedules - (schedule-map->cron-strings schedules))))) + (merge + {:name name + :engine engine + :details details + :is_full_sync full-sync? + :is_on_demand (boolean is_on_demand) + :description description + :caveats caveats + :points_of_interest points_of_interest + :auto_run_queries auto_run_queries} + (when schedules + (schedule-map->cron-strings schedules))))) (let [db (Database id)] (events/publish-event! :database-update db) ;; return the DB with the expanded schedules back in place diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj index f0c3697d3cc02a93d8119739e60b236ee30abcda..214711b7cff96dd42864c823df4e3f8c27ce99f2 100644 --- a/test/metabase/api/database_test.clj +++ b/test/metabase/api/database_test.clj @@ -13,6 +13,7 @@ [metabase.driver.util :as driver.u] [metabase.mbql.schema :as mbql.s] [metabase.models + [database :as database :refer [protected-password]] [permissions :as perms] [permissions-group :as perms-group]] [metabase.sync @@ -197,16 +198,17 @@ :features (driver.u/features :h2)} (into {} (db/select-one [Database :name :engine :details :is_full_sync], :id db-id))))))))) - (testing "should be able to set `auto_run_queries`" - (testing "when creating a Database" - (is (= {:auto_run_queries false} - (select-keys (create-db-via-api! {:auto_run_queries false}) [:auto_run_queries])))) - (testing "when updating a Database" - (mt/with-temp Database [{db-id :id} {:engine ::test-driver}] - (let [updates {:auto_run_queries false}] - ((mt/user->client :crowberto) :put 200 (format "database/%d" db-id) updates)) - (is (= false - (db/select-one-field :auto_run_queries Database, :id db-id)))))))) + (mt/with-log-level :info + (testing "should be able to set `auto_run_queries`" + (testing "when creating a Database" + (is (= {:auto_run_queries false} + (select-keys (create-db-via-api! {:auto_run_queries false}) [:auto_run_queries])))) + (testing "when updating a Database" + (mt/with-temp Database [{db-id :id} {:engine ::test-driver}] + (let [updates {:auto_run_queries false}] + ((mt/user->client :crowberto) :put 200 (format "database/%d" db-id) updates)) + (is (= false + (db/select-one-field :auto_run_queries Database, :id db-id))))))))) (deftest fetch-database-metadata-test (testing "GET /api/database/:id/metadata" @@ -941,3 +943,114 @@ (is (schema= [{:schema (s/eq schema-name) s/Keyword s/Any}] ((mt/user->client :rasta) :get 200 url)))))))))))) + +(deftest upsert-sensitive-values-test + (testing "empty maps are okay" + (is (= {} + (database-api/upsert-sensitive-fields {} {})))) + (testing "no details updates are okay" + (is (= nil + (database-api/upsert-sensitive-fields nil nil)))) + (testing "fields are replaced" + (is (= {:use-service-account nil + :dataset-id "dacort" + :use-jvm-timezone false + :service-account-json "{\"foo\": \"bar\"}" + :password "foo" + :pass "bar" + :tunnel-pass "quux" + :tunnel-private-key "foobar" + :tunnel-private-key-passphrase "fooquux" + :access-token "foobarfoo" + :refresh-token "foobarquux"} + (database-api/upsert-sensitive-fields {:description nil + :name "customer success BQ" + :details {:use-service-account nil + :dataset-id "dacort" + :service-account-json "{}" + :use-jvm-timezone false + :password "password" + :pass "pass" + :tunnel-pass "tunnel-pass" + :tunnel-private-key "tunnel-private-key" + :tunnel-private-key-passphrase "tunnel-private-key-passphrase" + :access-token "access-token" + :refresh-token "refresh-token"} + :id 2} + {:service-account-json "{\"foo\": \"bar\"}" + :password "foo" + :pass "bar" + :tunnel-pass "quux" + :tunnel-private-key "foobar" + :tunnel-private-key-passphrase "fooquux" + :access-token "foobarfoo" + :refresh-token "foobarquux"})))) + (testing "no fields are replaced" + (is (= {:use-service-account nil + :dataset-id "dacort" + :use-jvm-timezone false + :service-account-json "{}" + :password "password" + :pass "pass" + :tunnel-pass "tunnel-pass" + :tunnel-private-key "tunnel-private-key" + :tunnel-private-key-passphrase "tunnel-private-key-passphrase" + :access-token "access-token" + :refresh-token "refresh-token"} + (database-api/upsert-sensitive-fields {:description nil + :name "customer success BQ" + :details {:use-service-account nil + :dataset-id "dacort" + :use-jvm-timezone false + :service-account-json "{}" + :password "password" + :pass "pass" + :tunnel-pass "tunnel-pass" + :tunnel-private-key "tunnel-private-key" + :tunnel-private-key-passphrase "tunnel-private-key-passphrase" + :access-token "access-token" + :refresh-token "refresh-token"} + :id 2} + {:service-account-json protected-password + :password protected-password + :pass protected-password + :tunnel-pass protected-password + :tunnel-private-key protected-password + :tunnel-private-key-passphrase protected-password + :access-token protected-password + :refresh-token protected-password})))) + + (testing "only one field is replaced" + (is (= {:use-service-account nil + :dataset-id "dacort" + :use-jvm-timezone false + :service-account-json "{}" + :password "new-password" + :pass "pass" + :tunnel-pass "tunnel-pass" + :tunnel-private-key "tunnel-private-key" + :tunnel-private-key-passphrase "tunnel-private-key-passphrase" + :access-token "access-token" + :refresh-token "refresh-token"} + (database-api/upsert-sensitive-fields {:description nil + :name "customer success BQ" + :details {:use-service-account nil + :dataset-id "dacort" + :use-jvm-timezone false + :service-account-json "{}" + :password "password" + :pass "pass" + :tunnel-pass "tunnel-pass" + :tunnel-private-key "tunnel-private-key" + :tunnel-private-key-passphrase "tunnel-private-key-passphrase" + :access-token "access-token" + :refresh-token "refresh-token"} + :id 2} + {:service-account-json protected-password + :password "new-password" + :pass protected-password + :tunnel-pass protected-password + :tunnel-private-key protected-password + :tunnel-private-key-passphrase protected-password + :access-token protected-password + :refresh-token protected-password}))))) \ No newline at end of file