Skip to content
Snippets Groups Projects
Unverified Commit d514791a authored by Robert Roland's avatar Robert Roland Committed by GitHub
Browse files

"upsert" sensitive password-like fields during DB update (#13587)

When a database is updated via the admin interface, any sensitive data
(private keys, passwords, etc) should be handled on an "upsert" basis -
only modify those fields if they were changed from the "protected
password" value.

This affects all database drivers.

Resolves #13442
parent dc39729d
No related merge requests found
......@@ -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;
......
......@@ -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
......
......@@ -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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment