diff --git a/src/metabase/analytics/snowplow.clj b/src/metabase/analytics/snowplow.clj index 8ce54d54452b11ef7fb372a8bc5d3a0a8721309b..5b07e784fd9372908ccb387bc1f4e5015dfa2f65 100644 --- a/src/metabase/analytics/snowplow.clj +++ b/src/metabase/analytics/snowplow.clj @@ -85,6 +85,7 @@ (deferred-tru (str "Unique identifier to be used in Snowplow analytics, to identify this instance of Metabase. " "This is a public setting since some analytics events are sent prior to initial setup.")) + :encryption :never :visibility :public :base setting/uuid-nonce-base :doc false) diff --git a/src/metabase/core.clj b/src/metabase/core.clj index e198e3252eba5c595877b9e7c152b104bc444bf9..7c05d914eebe7b9828f9f11857a98717ea638aac 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -155,6 +155,7 @@ (ensure-audit-db-installed!) (init-status/set-progress! 0.95) + (settings/migrate-encrypted-settings!) ;; start scheduler at end of init! (task/start-scheduler!) (init-status/set-complete!) diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index a460f2e4cbd4f7604151a7686acbfe3acdaf1d94..e27e120c13682dd88ec07f0dd32b738ceacbb2b5 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -277,11 +277,6 @@ {:in encrypted-json-in :out cached-encrypted-json-out}) -(def transform-encrypted-text - "Transform for encrypted text." - {:in encryption/maybe-encrypt - :out encryption/maybe-decrypt}) - (defn normalize-visualization-settings "The frontend uses JSON-serialized versions of MBQL clauses as keys in `:column_settings`. This normalizes them to modern MBQL clauses so things work correctly." diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 8c913bca7e75719ea39796b575a53947e87442f3..398a074dd921caeba17978271f5218ea01864b66 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -84,13 +84,13 @@ [metabase.api.common :as api] [metabase.config :as config] [metabase.events :as events] - [metabase.models.interface :as mi] [metabase.models.serialization :as serdes] [metabase.models.setting.cache :as setting.cache] [metabase.plugins.classloader :as classloader] [metabase.server.middleware.json] [metabase.util :as u] [metabase.util.date-2 :as u.date] + [metabase.util.encryption :as encryption] [metabase.util.i18n :refer [deferred-trs deferred-tru trs tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] @@ -160,9 +160,6 @@ (methodical/defmethod t2/primary-keys :model/Setting [_model] [:key]) -(t2/deftransforms :model/Setting - {:value mi/transform-encrypted-text}) - (defmethod serdes/hash-fields :model/Setting [_setting] [:key]) @@ -274,6 +271,10 @@ ;; where this setting should be visible (default: :admin) [:visibility Visibility] + ;; should this setting be encrypted `:never` or `:maybe` (when `MB_ENCRYPTION_SECRET_KEY` is set). + ;; Defaults to `:maybe` + [:encryption [:enum :never :maybe]] + ;; should this setting be serialized? [:export? :boolean] @@ -327,7 +328,8 @@ (resolve-setting [k] (or (@registered-settings k) (throw (ex-info (tru "Unknown setting: {0}" k) - {:registered-settings + {::unknown-setting-error true + :registered-settings (sort (keys @registered-settings))}))))) (defn registered? @@ -386,6 +388,9 @@ (when (allows-database-local-values? setting) (core/get *database-local-values* setting-name)))) +(defn- prohibits-encryption? [setting] + (#{:never} (:encryption (resolve-setting setting)))) + (defn- allows-user-local-values? [setting] (#{:only :allowed} (:user-local (resolve-setting setting)))) @@ -974,6 +979,7 @@ :init nil :tag (default-tag-for-type setting-type) :visibility :admin + :encryption :maybe :export? false :sensitive? false :cache? true @@ -1518,3 +1524,46 @@ (:parse-error invalid-setting))) (log/warn (:parse-error invalid-setting) (format "Unable to parse setting %s" (:name invalid-setting)))))) + +(defn migrate-encrypted-settings! + "We have some settings that may currently be encrypted in the database that we'd like to disable encryption for. + This function just goes through all of them, checks to see if a value exists in the database, and re-saves it if + so. Toucan will handle decryption on the way out (if necessary) and the new value won't be encrypted. + + Note that we're completely working around the standard getters/setters here. This should be fine in this case + because: + - we're only doing anything when a value exists in the database, and + - we're setting the value to the exact same value that already exists - just a decrypted version." + [] + (doseq [setting (filter prohibits-encryption? (vals @registered-settings))] + ;; use a raw query to use `:for :update` + (t2/with-transaction [_conn] + (when-let [v (t2/select-one-fn :value :setting :key (setting-name setting) {:for :update})] + (when (not= (encryption/maybe-decrypt v) v) + ;; similarly, use `:setting` vs `:model/Setting` here to ensure the update is actually run even though Toucan + ;; thinks nothing has changed + (t2/update! :setting :key (setting-name setting) {:value (encryption/maybe-decrypt v)})))))) + +(defn- maybe-encrypt [setting-model] + ;; In tests, sometimes we need to insert/update settings that don't have definitions in the code and therefore can't + ;; be resolved. Fall back to maybe-encrypting these. + (let [resolved (try (resolve-setting (:key setting-model)) + (catch clojure.lang.ExceptionInfo e + (when (not (::unknown-setting-error (ex-data e))) + (throw e))))] + (cond-> setting-model + (or (nil? resolved) + (not (prohibits-encryption? resolved))) + (update :value encryption/maybe-encrypt)))) + +(t2/define-before-update :model/Setting + [setting] + (maybe-encrypt setting)) + +(t2/define-before-insert :model/Setting + [setting] + (maybe-encrypt setting)) + +(t2/define-after-select :model/Setting + [setting] + (update setting :value encryption/maybe-decrypt)) diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 0b37da0bb0b72397c9dfa963cd00e6f23df0be81..72493d73d1b106403ad755071192707b0b59013e 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -211,6 +211,7 @@ (application-name-for-setting-descriptions)) :type :boolean :default true + :encryption :never :visibility :public :audit :getter)