Settings are a fast and simple way to create a setting that can be set from the admin page. They are saved to the application Database, but intelligently cached internally for super-fast lookups. Define a new Setting with [[defsetting]] (optionally supplying things like default value, type, or custom getters & setters): (defsetting mandrill-api-key "API key for Mandrill") The newly-defined Setting will automatically be made available to the frontend client depending on its [[Visibility]]. You can also set the value via the corresponding env var, which looks like The var created with [[defsetting]] can be used as a getter/setter, or you can use [[get]] and [[set!]]: (require '[metabase.models.setting :as setting]) (setting/get :mandrill-api-key) ; only returns values set explicitly from the Admin Panel (mandrill-api-key) ; returns value set in the Admin Panel, OR value of corresponding env var, ; OR the default value, if any (in that order) (setting/set! :mandrill-api-key "NEW_KEY") (mandrill-api-key! "NEW_KEY") (setting/set! :mandrill-api-key nil) (mandrill-api-key! nil) You can define additional Settings types adding implementations of [[default-tag-for-type]], [[get-value-of-type]], and [[set-value-of-type!]]. [[writable-settings]] and [[user-readable-values-map]] can be used to fetch all Admin-writable and User-readable Settings, respectively. See their docstrings for more information. User-local and Database-local SettingsStarting in 0.42.0, some Settings are allowed to have Database-specific values that override the normal site-wide value. Similarly, starting in 0.43.0, some Settings are allowed to have User-specific values. These are similar in concept to buffer-local variables in Emacs Lisp. When a Setting is allowed to be User or Database local, any values in [[user-local-values]] or
[[database-local-values]] for that Setting will be returned preferentially to site-wide values of that Setting.
[[user-local-values]] comes from the Whether or not a Setting can be User- or Database-local is controlled by the
If a User-local setting is written in the context of an API request (i.e., when [[metabase.api.common/current-user]] is bound), the value will be local to the current user. If it is written outside of an API request, a site-wide value will be written. (At the time of this writing, there is not yet a FE-client-friendly way to set Database-local values. Just set them manually in the application DB until we figure that out.) Custom setter functions do not affect User- or Database-local values; they always set the site-wide value. See #14055 and #19399 for more information about and motivation behind User- and Database-local Settings. | (ns metabase.models.setting (:refer-clojure :exclude [get]) (:require [clojure.core :as core] [clojure.data :as data] [clojure.data.csv :as csv] [clojure.string :as str] [environ.core :as env] [malli.core :as mc] [medley.core :as m] [metabase.api.common :as api] [metabase.config :as config] [metabase.events :as events] [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.json :as json] [metabase.util.log :as log] [metabase.util.malli :as mu] [methodical.core :as methodical] [toucan2.core :as t2]) (:import (clojure.lang Keyword) (com.fasterxml.jackson.core JsonParseException) (com.fasterxml.jackson.core.io JsonEOFException) (java.io StringWriter) (java.time.temporal Temporal) (java.util.concurrent TimeUnit) (java.util.concurrent.locks ReentrantLock))) |
this namespace is required for side effects since it has the JSON encoder definitions for | (comment metabase.server.middleware.json/keep-me) |
Database-local Settings values (as a map of Setting name -> already-deserialized value). This comes from the value of
This is normally bound automatically in Query Processor context by [[metabase.query-processor.setup/do-with-database-local-settings]]. You may need to manually bind it in other places where you want to use Database-local values. | (def ^:dynamic *database-local-values* nil) |
User-local Settings values (as a map of Setting name -> already-deserialized value). This comes from the value of
This is a delay so that the settings for a user are loaded only if and when they are actually needed during a given API request. This is normally bound automatically by session middleware, in [[metabase.server.middleware.session]]. | (def ^:dynamic *user-local-values* (delay (atom nil))) |
A set of setting names which existed in previous versions of Metabase, but are no longer used. New settings may not use these names to avoid unintended side-effects if an application database still stores values for these settings. | (def ^:private retired-setting-names #{"-site-url" "enable-advanced-humanization" "metabot-enabled" "ldap-sync-admin-group" "user-recent-views" "most-recently-viewed-dashboard"}) |
A dynamic val that controls whether it's allowed to use retired settings. Primarily used in test to disable retired setting check. | (def ^:dynamic *allow-retired-setting-names* false) |
(declare admin-writable-site-wide-settings get-value-of-type set-value-of-type!) | |
Used to be the toucan1 model name defined using [[toucan.models/defmodel]], now it's a reference to the toucan2 model name. We'll keep this till we replace all the symbols in our codebase. | (def Setting :model/Setting) |
(methodical/defmethod t2/table-name :model/Setting [_model] :setting) | |
(doto :model/Setting (derive :metabase/model)) | |
(methodical/defmethod t2/primary-keys :model/Setting [_model] [:key]) | |
(defmethod serdes/hash-fields :model/Setting [_setting] [:key]) | |
(declare export?) | |
(defmethod serdes/extract-all "Setting" [_model _opts] (for [{:keys [key value]} (admin-writable-site-wide-settings :getter (partial get-value-of-type :string)) :when (export? key)] {:serdes/meta [{:model "Setting" :id (name key)}] :key key :value value})) | |
(defmethod serdes/load-find-local "Setting" [[{:keys [id]}]] (get-value-of-type :string (keyword id))) | |
(defmethod serdes/load-one! "Setting" [{:keys [key value]} _] (set-value-of-type! :string key value)) | |
(def ^:private Type [:fn {:error/message "Valid Setting :type"} (fn [a-type] (contains? (set (keys (methods get-value-of-type))) a-type))]) | |
(def ^:private Visibility [:enum :public :authenticated :settings-manager :admin :internal]) | |
Type tag that will be included in the Setting's metadata, so that the getter function will not cause reflection warnings. | (defmulti default-tag-for-type {:arglists '([setting-type])} keyword) |
(defmethod default-tag-for-type :default [_] `Object) (defmethod default-tag-for-type :string [_] `String) (defmethod default-tag-for-type :boolean [_] `Boolean) (defmethod default-tag-for-type :integer [_] `Long) (defmethod default-tag-for-type :double [_] `Double) (defmethod default-tag-for-type :timestamp [_] `Temporal) (defmethod default-tag-for-type :keyword [_] `Keyword) | |
Check whether the | (defn- validate-default-value-for-type [{:keys [tag default] :as _setting-definition}] ;; the errors below don't need to be i18n'ed since they're definition-time errors rather than user-facing (when (some? tag) (assert ((some-fn symbol? string?) tag) (format "Setting :tag should be a symbol or string, got: ^%s %s" (.getCanonicalName (class tag)) (pr-str tag)))) (when (and (some? default) (some? tag)) (let [klass (if (string? tag) (try (Class/forName tag) (catch Throwable e e)) (resolve tag))] (when-not (class? klass) (throw (ex-info (format "Cannot resolve :tag %s to a class. Is it fully qualified?" (pr-str tag)) {:tag klass} (when (instance? Throwable klass) klass)))) (when-not (instance? klass default) (throw (ex-info (format "Wrong :default type: got ^%s %s, but expected a %s" (.getCanonicalName (class default)) (pr-str default) (.getCanonicalName ^Class klass)) {:tag klass})))))) |
Schema for valid values of This is called | (def ^:private LocalOption [:enum :only :allowed :never]) |
(def ^:private SettingDefinition [:map [:name :keyword] [:munged-name :string] [:namespace :symbol] ;; description is validated via the macro, not schema ;; Use `:doc` to include a map with additional documentation, for use when generating the environment variable docs ;; from source. To exclude a setting from documenation, set to `false`. See metabase.cmd.env-var-dox. [:description :any] [:doc :any] [:default :any] ;; all values are stored in DB as Strings, [:type Type] ;; different getters/setters take care of parsing/unparsing [:getter ifn?] [:setter ifn?] ;; an init function can be used to seed initial values [:init [:maybe ifn?]] ;; type annotation, e.g. ^String, to be applied. Defaults to tag based on :type [:tag :symbol] ;; is this sensitive (never show in plaintext), like a password? (default: false) [:sensitive? :boolean] ;; where this setting should be visible (default: :admin) [:visibility Visibility] ;; should this setting be encrypted. Available options are `:no` or `:when-encryption-key-set` (the setting will be ;; encrypted when `MB_ENCRYPTION_SECRET_KEY` is set, otherwise we can't encrypt). This is required for `:timestamp`, ;; `:json`, and `:csv`-typed settings. Defaults to `:no` for all other types. [:encryption [:enum :no :when-encryption-key-set]] ;; should this setting be serialized? [:export? :boolean] ;; should the getter always fetch this value "fresh" from the DB? (default: false) [:cache? :boolean] ;; if non-nil, contains the Metabase version in which this setting was deprecated [:deprecated [:maybe :string]] ;; whether this Setting can be Database-local or User-local. See [[metabase.models.setting]] docstring for more info. [:database-local LocalOption] [:user-local LocalOption] ;; called whenever setting value changes, whether from update-setting! or a cache refresh. used to handle cases ;; where a change to the cache necessitates a change to some value outside the cache, like when a change the ;; `:site-locale` setting requires a call to `java.util.Locale/setDefault` [:on-change [:maybe ifn?]] ;; If non-nil, determines the Enterprise feature flag required to use this setting. If the feature is not enabled, ;; the setting will behave the same as if `enabled?` returns `false` (see below). [:feature [:maybe :keyword]] ;; Function which returns true if the setting should be enabled. If it returns false, the setting will throw an ;; exception when it is attempted to be set, and will return its default value when read. Defaults to always enabled. [:enabled? [:maybe ifn?]] ;; Keyword that determines what kind of audit log entry should be created when this setting is written. Options are ;; `:never`, `:no-value`, `:raw-value`, and `:getter`. User- and database-local settings are never audited. `:getter` ;; should be used for most non-sensitive settings, and will log the value returned by its getter, which may be a ;; the default getter or a custom one. ;; (default: `:no-value`) [:audit [:maybe [:enum :never :no-value :raw-value :getter]]]]) | |
Map of loaded defsettings | (defonce registered-settings (atom {})) |
(defprotocol ^:private Resolvable (resolve-setting [setting-definition-or-name] "Resolve the definition map for a Setting. `setting-definition-or-name` map be a map, keyword, or string.")) | |
(extend-protocol Resolvable clojure.lang.IPersistentMap (resolve-setting [this] this) String (resolve-setting [s] (resolve-setting (keyword s))) clojure.lang.Keyword (resolve-setting [k] (or (@registered-settings k) (throw (ex-info (tru "Unknown setting: {0}" k) {::unknown-setting-error true :registered-settings (sort (keys @registered-settings))}))))) | |
Returns true if a setting is registered with the given name. | (defn registered? [setting-keyword-or-name] (contains? @registered-settings (keyword setting-keyword-or-name))) |
The actual watch that triggers this happens in [[metabase.models.setting.cache/cache*]] because the cache might be swapped out depending on which app DB we have in play this isn't really something that needs to be a multimethod, but I'm using it because the logic can't really live in [[metabase.models.setting.cache]] but the cache has to live here; this is a good enough way to prevent circular references for now | (defmethod setting.cache/call-on-change :default [old new] (let [rs @registered-settings [d1 d2] (data/diff old new)] (doseq [changed-setting (into (set (keys d1)) (set (keys d2)))] (when-let [on-change (get-in rs [(keyword changed-setting) :on-change])] (on-change (core/get old changed-setting) (core/get new changed-setting)))))) |
+----------------------------------------------------------------------------------------------------------------+ | get | +----------------------------------------------------------------------------------------------------------------+ | |
(defprotocol ^:private SettingName (setting-name ^String [setting-definition-or-name] "String name of a Setting, e.g. `\"site-url\"`. Works with strings, keywords, or Setting definition maps.")) | |
(extend-protocol SettingName clojure.lang.IPersistentMap (setting-name [this] (name (:name this))) String (setting-name [this] this) clojure.lang.Keyword (setting-name [this] (name this))) | |
(defn- database-local-only? [setting] (= (:database-local (resolve-setting setting)) :only)) | |
(defn- user-local-only? [setting] (= (:user-local (resolve-setting setting)) :only)) | |
(defn- allows-database-local-values? [setting] (#{:only :allowed} (:database-local (resolve-setting setting)))) | |
(defn- database-local-value [setting-definition-or-name] (let [{setting-name :name, :as setting} (resolve-setting setting-definition-or-name)] (when (allows-database-local-values? setting) (core/get *database-local-values* setting-name)))) | |
(defn- prohibits-encryption? [setting-or-name] (= :no (:encryption (resolve-setting setting-or-name)))) | |
(defn- allows-user-local-values? [setting] (#{:only :allowed} (:user-local (resolve-setting setting)))) | |
(defn- allows-site-wide-values? [setting] (and (not (database-local-only? setting)) (not (user-local-only? setting)))) | |
(defn- site-wide-only? [setting] (and (not (allows-database-local-values? setting)) (not (allows-user-local-values? setting)))) | |
(defn- user-local-value [setting-definition-or-name] (let [{setting-name :name, :as setting} (resolve-setting setting-definition-or-name)] (when (allows-user-local-values? setting) (core/get @@*user-local-values* setting-name)))) | |
(defn- should-set-user-local-value? [setting-definition-or-name] (let [setting (resolve-setting setting-definition-or-name)] (and (allows-user-local-values? setting) @@*user-local-values*))) | |
(defn- set-user-local-value! [setting-definition-or-name value] (let [{setting-name :name} (resolve-setting setting-definition-or-name)] ;; Update the atom in *user-local-values* with the new value before writing to the DB. This ensures that ;; subsequent setting updates within the same API request will not overwrite this value. (swap! @*user-local-values* u/assoc-dissoc setting-name value) (t2/update! 'User api/*current-user-id* {:settings (json/encode @@*user-local-values*)}))) | |
A dynamic var that controls whether we should enforce checks on setting access. Defaults to false; should be set to true when settings are being written directly via /api/setting endpoints. | (def ^:dynamic *enforce-setting-access-checks* false) |
(defn- has-feature? [feature] (u/ignore-exceptions (classloader/require 'metabase.public-settings.premium-features)) (let [has-feature?' (resolve 'metabase.public-settings.premium-features/has-feature?)] (has-feature?' feature))) | |
If | (defn has-advanced-setting-access? [] (or api/*is-superuser?* (do (when config/ee-available? (classloader/require 'metabase-enterprise.advanced-permissions.common 'metabase.public-settings.premium-features)) (if-let [current-user-has-application-permissions? (and (has-feature? :advanced-permissions) (resolve 'metabase-enterprise.advanced-permissions.common/current-user-has-application-permissions?))] (current-user-has-application-permissions? :setting) false)))) |
This checks whether the current user should have the ability to read or write the provided setting. By default this function always returns | (defn- current-user-can-access-setting? [setting] (or (not *enforce-setting-access-checks*) (nil? api/*current-user-id*) api/*is-superuser?* (and ;; Non-admin setting managers can only access settings that are not marked as admin-only (not api/*is-superuser?*) (has-advanced-setting-access?) (not= (:visibility setting) :admin)) (and ;; Non-admins can only access user-local settings not marked as admin-only (allows-user-local-values? setting) (not= (:visibility setting) :admin)))) |
Munge names so that they are legal for bash. Only allows for alphanumeric characters, underscores, and hyphens. | (defn- munge-setting-name [setting-nm] (str/replace (name setting-nm) #"[^a-zA-Z0-9_-]*" "")) |
Get the env var corresponding to | (defn- env-var-name ^String [setting-definition-or-name] (str "MB_" (-> (setting-name setting-definition-or-name) munge-setting-name (str/replace "-" "_") u/upper-case-en))) |
Correctly translate a setting to the keyword it will be found at in [[env/env]]. | (defn setting-env-map-name [setting-definition-or-name] (keyword (str "mb-" (munge-setting-name (setting-name setting-definition-or-name))))) |
Get the value of | (defn env-var-value ^String [setting-definition-or-name] (let [setting (resolve-setting setting-definition-or-name)] (when (allows-site-wide-values? setting) (let [v (env/env (setting-env-map-name setting))] (when (seq v) v))))) |
(def ^:private ^:dynamic *disable-init* false) | |
(declare get) (declare set!) | |
Fetch the value of Note: This will bypass initialization, i.e. it could return nil for a nonce | (defn read-setting [setting-definition-or-name] (binding [*disable-init* true] (get setting-definition-or-name))) |
(defn- db-value [setting-definition-or-name] (t2/select-one-fn :value Setting :key (setting-name setting-definition-or-name))) | |
(defn- db-is-set-up? [] ;; this should never be hit. it is just overly cautious against a NPE here. But no way this cannot resolve (let [f (requiring-resolve 'metabase.db/db-is-set-up?)] (if f (f) false))) | |
Get the value, if any, of | (defn- db-or-cache-value ^String [setting-definition-or-name] (let [setting (resolve-setting setting-definition-or-name)] ;; cannot use db (and cache populated from db) if db is not set up (when (and (db-is-set-up?) (allows-site-wide-values? setting)) (not-empty (if config/*disable-setting-cache* (db-value setting) (do ;; gotcha - returns immediately if another process is restoring it, i.e. before it's been populated (setting.cache/restore-cache-if-needed!) (let [cache (setting.cache/cache)] (if (nil? cache) ;; nil if we returned early above, and the cache is still being restored - in that case hit the db (db-value setting) (core/get cache (setting-name setting-definition-or-name)))))))))) |
(defonce ^:private ^ReentrantLock init-lock (ReentrantLock.)) | |
(defn- init! [setting-definition-or-name] (let [{:keys [init] :as setting} (resolve-setting setting-definition-or-name)] (when init (when (not (db-is-set-up?)) (throw (ex-info "Cannot initialize setting before the db is set up" {:setting setting}))) ;; We do not need to interact with the restore-cache-lock as it is OK to race with it. (if-not (.tryLock init-lock 30 TimeUnit/SECONDS) (throw (ex-info "Unable to get initialization lock" {:setting setting-definition-or-name})) (try (u/or-with some? ;; perhaps another process initialized this setting while we were waiting for the lock (read-setting setting) (when init (when-let [init-value (init)] (metabase.models.setting/set! setting init-value :bypass-read-only? true)))) (finally (.unlock init-lock))))))) | |
Parsing a setting may result in a lazy value. Use this to ensure we finish parsing. | (defn- realize [value] (when (coll? value) (dorun (map realize value))) value) |
Get the | (defn default-value [setting-definition-or-name] (let [{:keys [default]} (resolve-setting setting-definition-or-name)] default)) |
Get the raw value of a Setting from wherever it may be specified. Value is fetched by trying the following sources in order:
!!!!!!!!!! The value returned MAY OR MAY NOT be a String depending on the source !!!!!!!!!! This is the underlying function powering all the other getters such as methods of [[get-value-of-type]]. These getter functions must be coded to handle either String or non-String values. You can use the three-arity version of this function to do that. Three-arity version can be used to specify how to parse non-empty String values ( | (defn get-raw-value ([setting-definition-or-name] (let [setting (resolve-setting setting-definition-or-name) source-fns [user-local-value database-local-value env-var-value db-or-cache-value (cond (some? (:default setting)) default-value (:init setting) (when-not *disable-init* init!))]] (loop [[f & more] source-fns] (let [v (when f (f setting))] (cond (some? v) v (seq more) (recur more)))))) ([setting-definition-or-name pred parse-fn] (let [parse (fn [v] (try (realize (parse-fn v)) (catch Throwable e (let [{setting-name :name} (resolve-setting setting-definition-or-name)] (throw (ex-info (tru "Error parsing Setting {0}: {1}" setting-name (ex-message e)) {:setting setting-name} e)))))) raw-value (get-raw-value setting-definition-or-name) v (cond-> raw-value (string? raw-value) parse)] (when (pred v) v)))) |
Get the value of Impls should call [[get-raw-value]] to get the underlying possibly-serialized value and parse it appropriately if it
comes back as a String; impls should only return values that are of the correct type (e.g. the | (defmulti get-value-of-type {:arglists '([setting-type setting-definition-or-name])} (fn [setting-type _] (keyword setting-type))) |
(defmethod get-value-of-type :string [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name string? identity)) | |
(mu/defn string->boolean :- [:maybe :boolean] "Interpret a `string-value` of a Setting as a boolean." [string-value :- [:maybe :string]] (when (seq string-value) (case (u/lower-case-en string-value) "true" true "false" false (throw (Exception. (tru "Invalid value for string: must be either \"true\" or \"false\" (case-insensitive).")))))) | |
(defn- random-uuid-str [] (str (random-uuid))) | |
A random uuid value that should never change again This base allows the bundling of a number of attributes together. In some sense it is defining a subtype / mixin. | (def uuid-nonce-base {:type :string :setter :none :audit :never :init random-uuid-str}) |
Strings are parsed as follows:
| (defmethod get-value-of-type :boolean [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name boolean? string->boolean)) |
(defmethod get-value-of-type :integer [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name integer? #(Long/parseLong ^String %))) | |
(defmethod get-value-of-type :positive-integer [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name pos-int? #(Long/parseLong ^String %))) | |
(defmethod get-value-of-type :double [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name double? #(Double/parseDouble ^String %))) | |
(defmethod get-value-of-type :keyword [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name keyword? keyword)) | |
(defmethod get-value-of-type :timestamp [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name #(instance? Temporal %) u.date/parse)) | |
(defmethod get-value-of-type :json [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name coll? json/decode+kw)) | |
(defmethod get-value-of-type :csv [_setting-type setting-definition-or-name] (get-raw-value setting-definition-or-name sequential? (comp first csv/read-csv))) | |
(defn- default-getter-for-type [setting-type] (partial get-value-of-type (keyword setting-type))) | |
Fetch the value of Note: If the setting has an initializer, and this is the first time accessing, a value will be generated and saved unless disable-init has been bound to a truthy value. | (defn get [setting-definition-or-name] (let [{:keys [cache? getter enabled? default feature]} (resolve-setting setting-definition-or-name) disable-cache? (or config/*disable-setting-cache* (not cache?))] (if (or (and feature (not (has-feature? feature))) (and enabled? (not (enabled?)))) default (binding [config/*disable-setting-cache* disable-cache?] (getter))))) |
+----------------------------------------------------------------------------------------------------------------+ | set! | +----------------------------------------------------------------------------------------------------------------+ | |
Update an existing Setting. Used internally by [[set-value-of-type!]] for | (defn- update-setting! [setting-name new-value] (assert (not= setting-name setting.cache/settings-last-updated-key) (tru "You cannot update `settings-last-updated` yourself! This is done automatically.")) ;; Toucan 2 version of `update!` will do transforms and stuff like that (t2/update! Setting :key setting-name {:value new-value})) |
Insert a new row for a Setting. Used internally by [[set-value-of-type!]] for | (defn- set-new-setting! [setting-name new-value] (try (first (t2/insert-returning-instances! Setting :key setting-name :value new-value)) ;; if for some reason inserting the new value fails it almost certainly means the cache is out of date ;; and there's actually a row in the DB that's not in the cache for some reason. Go ahead and update the ;; existing value and log a warning (catch Throwable e (log/warn "Error inserting a new Setting:\n" (ex-message e) "\n" "Assuming Setting already exists in DB and updating existing value.") (update-setting! setting-name new-value)))) |
(defn- obfuscated-value? [v] (when (seq v) (boolean (re-matches #"^\*{10}.{2}$" v)))) | |
Obfuscate the value of sensitive Setting. We'll still show the last 2 characters so admins can still check that the value is what's expected (e.g. the correct password). (obfuscate-value "sensitivePASSWORD123") ;; -> "**23" | (defn obfuscate-value [s] (str "**********" (str/join (take-last 2 (str s))))) |
Set the value of a Impls of this method should ultimately call the implementation for | (defmulti set-value-of-type! {:arglists '([setting-type setting-definition-or-name new-value])} (fn [setting-type _ _] (keyword setting-type))) |
(mu/defmethod set-value-of-type! :string [_setting-type setting-definition-or-name new-value :- [:maybe :string]] (let [new-value (when (seq new-value) new-value) {:keys [sensitive? deprecated] :as setting} (resolve-setting setting-definition-or-name) obfuscated? (and sensitive? (obfuscated-value? new-value)) setting-name (setting-name setting)] ;; if someone attempts to set a sensitive setting to an obfuscated value (probably via a misuse of the `set-many!` function, setting values that have not changed), ignore the change. Log a message that we are ignoring it. (if obfuscated? (log/infof "Attempted to set Setting %s to obfuscated value. Ignoring change." setting-name) (do (when (and deprecated (not (nil? new-value))) (log/warnf "Setting %s is deprecated as of Metabase %s and may be removed in a future version." setting-name deprecated)) (when (and (= :only (:user-local setting)) (not (should-set-user-local-value? setting))) (log/warnf "Setting %s can only be set in a user-local way, but there are no *user-local-values*." setting-name)) (if (should-set-user-local-value? setting) ;; If this is user-local and this is being set in the context of an API call, we don't want to update the ;; site-wide value or write or read from the cache (set-user-local-value! setting-name new-value) (do ;; make sure we're not trying to set the value of a Database-local-only Setting (when-not (allows-site-wide-values? setting) (throw (ex-info (tru "Site-wide values are not allowed for Setting {0}" (:name setting)) {:setting (:name setting)}))) ;; always update the cache entirely when updating a Setting. (setting.cache/restore-cache!) ;; write to DB (cond (nil? new-value) (t2/delete! (t2/table-name Setting) :key setting-name) ;; if there's a value in the cache then the row already exists in the DB; update that (contains? (setting.cache/cache) setting-name) (update-setting! setting-name new-value) ;; if there's nothing in the cache then the row doesn't exist, insert a new one :else (set-new-setting! setting-name new-value)) ;; update cached value (setting.cache/update-cache! setting-name new-value) ;; Record the fact that a Setting has been updated so eventually other instances (if applicable) find out ;; about it (For Settings that don't use the Cache, don't update the `last-updated` value, because it will ;; cause other instances to do needless reloading of the cache from the DB) (when-not config/*disable-setting-cache* (setting.cache/update-settings-last-updated!)))) ;; Now return the `new-value`. new-value)))) | |
(defmethod set-value-of-type! :keyword [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (u/qualified-name new-value))) | |
(defmethod set-value-of-type! :boolean [setting-type setting-definition-or-name new-value] (if (string? new-value) (set-value-of-type! setting-type setting-definition-or-name (string->boolean new-value)) (let [s (case new-value true "true" false "false" nil nil)] (set-value-of-type! :string setting-definition-or-name s)))) | |
(defmethod set-value-of-type! :integer [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (when new-value (assert (or (integer? new-value) (and (string? new-value) (re-matches #"^-?\d+$" new-value)))) (str new-value)))) | |
(defmethod set-value-of-type! :positive-integer [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (when new-value (assert (or (pos-int? new-value) (and (string? new-value) (re-matches #"^[1-9]\d*$" new-value)))) (str new-value)))) | |
(defmethod set-value-of-type! :double [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (when new-value (assert (or (number? new-value) (and (string? new-value) (re-matches #"[+-]?([0-9]*[.])?[0-9]+" new-value)))) (str new-value)))) | |
(defmethod set-value-of-type! :json [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (some-> new-value json/encode))) | |
(defmethod set-value-of-type! :timestamp [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (some-> new-value u.date/format))) | |
(defn- serialize-csv [value] (cond ;; if we're passed as string, assume it's already CSV-encoded (string? value) value (sequential? value) (let [s (with-open [writer (StringWriter.)] (csv/write-csv writer [value]) (str writer))] (first (str/split-lines s))) :else value)) | |
(defmethod set-value-of-type! :csv [_setting-type setting-definition-or-name new-value] (set-value-of-type! :string setting-definition-or-name (serialize-csv new-value))) | |
(defn- default-setter-for-type [setting-type] (partial set-value-of-type! (keyword setting-type))) | |
(defn- audit-setting-change! [{:keys [name audit sensitive?]} previous-value new-value] (let [maybe-obfuscate #(cond-> % sensitive? obfuscate-value)] (events/publish-event! :event/setting-update {:details (merge {:key name} (when (not= audit :no-value) {:previous-value (maybe-obfuscate previous-value) :new-value (maybe-obfuscate new-value)})) :user-id api/*current-user-id* :model :model/Setting}))) | |
Returns true if the setting change should be written to the | (defn- should-audit? [setting] (not= (:audit setting) :never)) |
Calls the setting's setter with | (defn- set-with-audit-logging! [{:keys [getter audit setter] setting-type :type :as setting} new-value bypass-read-only?] (let [setter (if (and bypass-read-only? (= :none setter)) (partial set-value-of-type! setting-type setting) setter)] (if (should-audit? setting) (let [audit-value-fn #(condp = audit :no-value nil :raw-value (get-raw-value setting) :getter (getter)) previous-value (audit-value-fn)] (u/prog1 (setter new-value) (audit-setting-change! setting previous-value (audit-value-fn)))) (setter new-value)))) |
Set the value of (set :mandrill-api-key "xyz123") Style note: prefer using the setting directly instead: (mandrill-api-key "xyz123") This method will throw an exception if trying to update a read-only setting, unless | (defn set! [setting-definition-or-name new-value & {:keys [bypass-read-only?]}] (let [{:keys [setter cache? enabled? feature] :as setting} (resolve-setting setting-definition-or-name) name (setting-name setting)] (when (and feature (not (has-feature? feature))) (throw (ex-info (tru "Setting {0} is not enabled because feature {1} is not available" name feature) setting))) (when (and enabled? (not (enabled?))) (throw (ex-info (tru "Setting {0} is not enabled" name) setting))) (when-not (current-user-can-access-setting? setting) (throw (ex-info (tru "You do not have access to the setting {0}" name) setting))) (when-not bypass-read-only? (when (= setter :none) (throw (UnsupportedOperationException. (tru "You cannot set {0}; it is a read-only setting." name))))) (binding [config/*disable-setting-cache* (not cache?)] (set-with-audit-logging! setting new-value bypass-read-only?)))) |
Encryption is turned off or on according to (in order of preference):
If none of these conditions are met (a non- | (defn- extract-encryption-or-default [setting] (or (:encryption setting) ;; NOTE: if none of the below conditions is met, users of `defsetting` will be required to ;; provide a value for `:encryption`. ;; ;; if a setting is `:sensitive?`, default to encrypting it (when (:sensitive? setting) :when-encryption-key-set) ;; if a setting isn't stored in the DB, the value doesn't really matter, but provide ;; a default so the caller doesn't have to (when (= (:setter setting) :none) :when-encryption-key-set) ;; if the setting isn't a type likely to contain secrets, default to plaintext (when (contains? #{:boolean :integer :positive-integer :double :keyword :timestamp} (:type setting)) :no) (throw (ex-info (trs "`:encryption` is a required option for setting {0}" (:name setting)) {:setting setting})))) |
+----------------------------------------------------------------------------------------------------------------+ | register-setting! | +----------------------------------------------------------------------------------------------------------------+ | |
Register a new Setting with a map of [[SettingDefinition]] attributes. Returns the map it was passed. This is used internally by [[defsetting]]; you shouldn't need to use it yourself. | (defn register-setting! [{setting-name :name, setting-ns :namespace, setting-type :type, default :default, :as setting}] (let [munged-name (munge-setting-name (name setting-name))] (u/prog1 (let [setting-type (mc/assert Type (or setting-type :string))] (merge {:name setting-name :munged-name munged-name :namespace setting-ns :description nil :doc nil :type setting-type :default default :on-change nil :getter (partial (default-getter-for-type setting-type) setting-name) :setter (partial (default-setter-for-type setting-type) setting-name) :init nil :tag (default-tag-for-type setting-type) :visibility :admin :encryption (extract-encryption-or-default setting) :export? false :sensitive? false :cache? true :feature nil :database-local :never :user-local :never :deprecated nil :enabled? nil ;; Disable auditing by default for user- or database-local settings :audit (if (site-wide-only? setting) :no-value :never)} (dissoc setting :name :type :default))) (mc/assert SettingDefinition <>) (validate-default-value-for-type <>) ;; eastwood complains about (setting-name @registered-settings) for shadowing the function `setting-name` (when-let [registered-setting (core/get @registered-settings setting-name)] (when (not= setting-ns (:namespace registered-setting)) ;; not i18n'ed because this is supposed to be developer-facing only. (throw (ex-info (format "Setting %s already registered in %s. You can remove the old definition with (swap! %s dissoc %s)" setting-name (:namespace registered-setting) `registered-settings (keyword setting-name)) {:existing-setting (dissoc registered-setting :on-change :getter :setter)})))) (when-let [same-munge (first (filter (comp #{munged-name} :munged-name) (vals @registered-settings)))] (when (not= setting-name (:name same-munge)) ;; redefinitions are fine (throw (ex-info (tru "Setting names in would collide: {0} and {1}" setting-name (:name same-munge)) {:existing-setting (dissoc same-munge :on-change :getter :setter) :new-setting (dissoc <> :on-change :getter :setter)})))) (when (and (retired-setting-names (name setting-name)) (not *allow-retired-setting-names*)) (throw (ex-info (tru "Setting name ''{0}'' is retired; use a different name instead" (name setting-name)) {:retired-setting-name (name setting-name) :new-setting (dissoc <> :on-change :getter :setter)}))) (when (and (allows-user-local-values? setting) (allows-database-local-values? setting)) (throw (ex-info (tru "Setting {0} allows both user-local and database-local values; this is not supported" setting-name) {:setting setting}))) (when (and (some? (:default setting)) (:init setting)) (throw (ex-info (tru "Setting {0} uses both :default and :init options, which are mutually exclusive" setting-name) {:setting setting}))) (when (and (:enabled? setting) (:feature setting)) (throw (ex-info (tru "Setting {0} uses both :enabled? and :feature options, which are mutually exclusive" setting-name) {:setting setting}))) (swap! registered-settings assoc setting-name <>)))) |
+----------------------------------------------------------------------------------------------------------------+ | defsetting macro | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- setting-fn-docstring [{:keys [default description], setting-type :type, :as setting}] ;; indentation below is intentional to make it clearer what shape the generated documentation is going to take. (str (description) \newline \newline (format "`%s` is a `%s` Setting. You can get its value by calling:\n" (setting-name setting) setting-type) \newline (format " (%s)\n" (setting-name setting)) \newline "and set its value by calling:\n" \newline (format " (%s! <new-value>)\n" (setting-name setting)) \newline (format "You can also set its value with the env var `%s`.\n" (env-var-name setting)) \newline "Clear its value by calling:\n" \newline (format " (%s! nil)\n" (setting-name setting)) \newline (format "Its default value is `%s`." (pr-str default)))) | |
Impl for [[defsetting]]. Create metadata for [[setting-fn]]. | (defn setting-fn-metadata [getter-or-setter {:keys [tag deprecated], :as setting}] {:arglists (case getter-or-setter :getter (list (with-meta [] {:tag tag})) :setter (list (with-meta '[new-value] {:tag tag}))) :deprecated deprecated :doc (setting-fn-docstring setting)}) |
Impl for [[defsetting]]. Create the automatically defined | (defn setting-fn [getter-or-setter setting] (case getter-or-setter :getter (fn setting-getter* [] (get setting)) :setter (fn setting-setter* [new-value] ;; need to qualify this or otherwise the reader gets this confused with the set! used for things like ;; (set! *warn-on-reflection* true) ;; :refer-clojure :exclude doesn't seem to work in this case (metabase.models.setting/set! setting new-value)))) |
The next few functions are for validating the Setting description (i.e., docstring) at macroexpansion time. They check that the docstring is a valid deferred i18n form (e.g. [[metabase.util.i18n/deferred-tru]]) so the Setting description will be localized properly when it shows up in the FE admin interface. | |
(def ^:private allowed-deferred-i18n-forms #{`deferred-trs `deferred-tru}) | |
Whether (is-form? #{ | (defn- is-form? [symbols form] (when (and (list? form) (symbol? (first form))) ;; resolve the symbol to a var and convert back to a symbol so we can get the actual name rather than whatever ;; alias the current namespace happens to be using (let [symb (symbol (resolve (first form)))] ((set symbols) symb)))) |
(defn- valid-trs-or-tru? [desc] (is-form? allowed-deferred-i18n-forms desc)) | |
Check that | (defn- validate-description-form* [description-form] (when-not (valid-trs-or-tru? description-form) ;; this doesn't need to be i18n'ed because it's a compile-time error. `(ex-info (str "defsetting docstrings must be a *deferred* i18n form unless the Setting has" " `:visibility` `:internal`, `:setter` `:none`, or is defined in a test namespace." (format " Got: ^%s %s" (some-> ~description-form class (.getCanonicalName)) (pr-str ~description-form))) {:description-form ~description-form}))) |
This exists as its own method so that we can stub it in tests | (defn- ns-in-test? [ns-name] (str/ends-with? ns-name "-test")) |
(defn- requires-i18n? [setting-definition] (and (not= (:visibility setting-definition) :internal) (not= (:setter setting-definition) :none) (not (ns-in-test? (:namespace setting-definition))))) | |
Use values in the optional | (defn merge-base [{:keys [base] :as setting-options}] (if-not base setting-options (do (assert (not (contains? base :export)) ":export? must not be set in :base") (merge base (dissoc setting-options :base))))) |
Defines a new Setting that will be added to the DB at some point in the future. Conveniently can be used as a getter/setter as well (defsetting mandrill-api-key (trs "API key for Mandrill.")) (mandrill-api-key) ; get the value (mandrill-api-key! new-value) ; update the value (mandrill-api-key! nil) ; delete the value A setting can be set from the Admin Panel or via the corresponding env var, eg. You may optionally pass any of the `:default`The default value of the setting. This must be of the same type as the Setting type, e.g. the default for an
`:init`A optional 0-arity function which returns the same type as the Setting type, e.g. for an `:type`
`:visibility`Controls where this setting is visibile, and who can update it. Possible values are: Visibility | Who Can See It? | Who Can Update It? ---------------- | ---------------------------- | -------------------- :public | The entire world | Admins and Settings Managers :authenticated | Logged-in Users | Admins and Settings Managers :settings-manager| Admins and Settings Managers | Admins and Settings Managers :admin | Admins | Admins :internal | Nobody | No one (usually for env-var-only settings) 'Settings Managers' are non-admin users with the 'settings' permission, which gives them access to the Settings page in the Admin Panel. `:export?`Whether this Setting is included when producing a serializing settings export. `:getter`A custom getter fn, which takes no arguments. Overrides the default implementation. (This can in turn call functions in this namespace like methods of [[get-value-of-type]] to invoke the 'parent' getter behavior.) `:setter`A custom setter fn, which takes a single argument, or `:cache?`Should this Setting be cached? (default `:sensitive?`Is this a sensitive setting, such as a password, that we should never return in plaintext? (Default: `:database-local`The ability of this Setting to be /Database-local/. Valid values are `:user-local`Whether this Setting is /User-local/. Valid values are `:deprecated`If this setting is deprecated, this should contain a string of the Metabase version in which the setting was
deprecated. A deprecation notice will be logged whenever the setting is written. (Default: `:on-change`Do you want to update something else when this setting changes? Takes a function which takes 2 arguments, `:feature`If non-nil, determines the Enterprise feature flag required to use this setting. If the feature is not enabled,
the setting will behave the same as if `enabled?`Function which returns true if the setting should be enabled. If it returns false, the setting will throw an exception when it is attempted to be set, and will return its default value when read. Defaults to always enabled. `audit`Keyword that determines what kind of audit log entry should be created when this setting is written. Options are
`base`A map which can provide values for any of the above options, except for :export?. Any top level options will override what's in this base map. The use case for this map is sharing strongly coupled options between similar settings, see [[uuid-nonce-base]]. | (defmacro defsetting {:style/indent 1} [setting-symbol description & {:as options}] {:pre [(symbol? setting-symbol) (not (namespace setting-symbol)) ;; don't put exclamation points in your Setting names. We don't want functions like `exciting!` for the getter ;; and `exciting!!` for the setter. (not (str/includes? (name setting-symbol) "!"))]} (let [;; we need the compile-time description form to check whether it supports i18n ;; we only build the ex for now - we must check the runtime expanded setting-definition for whether i18n is required maybe-i18n-exception (validate-description-form* description) setting-metadata {:name (keyword setting-symbol) ;; wrap the description form in a thunk, so its result updates with its dependencies :description `(fn [] ~description) :namespace (list 'quote (ns-name *ns*))} ;; create symbols for the getter and setter functions e.g. `my-setting` and `my-setting!` respectively. ;; preserve metadata from the `setting-symbol` passed to `defsetting`. setting-getter-fn-symbol setting-symbol setting-setter-fn-symbol (-> (symbol (str (name setting-symbol) \!)) (with-meta (meta setting-symbol))) setting-definition-symbol (gensym "setting-")] `(let [setting-options# (merge (merge-base ~options) ~setting-metadata) ~setting-definition-symbol (register-setting! setting-options#)] ~(when maybe-i18n-exception `(when (#'requires-i18n? ~setting-definition-symbol) (throw ~maybe-i18n-exception))) (-> (def ~setting-getter-fn-symbol (setting-fn :getter ~setting-definition-symbol)) (alter-meta! merge (setting-fn-metadata :getter ~setting-definition-symbol))) ;; unfortunately we can't evaluate this condition at compile time, as the options might contain runtime forms. (when (not= (:setter ~setting-definition-symbol) :none) ;; therefore we need to do some runtime skullduggery to ensure the var is only created conditionally (-> (intern (:namespace ~setting-definition-symbol) '~setting-setter-fn-symbol (setting-fn :setter ~setting-definition-symbol)) (alter-meta! merge (setting-fn-metadata :setter ~setting-definition-symbol))))))) |
+----------------------------------------------------------------------------------------------------------------+ | EXTRA UTIL FNS | +----------------------------------------------------------------------------------------------------------------+ | |
Set the value of several Settings at once. (set-many! {:mandrill-api-key "xyz123", :another-setting "ABC"}) | (defn set-many! [settings] ;; if setting any of the settings fails, roll back the entire DB transaction and the restore the cache from the DB ;; to revert any changes in the cache (try (t2/with-transaction [_conn] (doseq [[k v] settings] (metabase.models.setting/set! k v))) settings (catch Throwable e (setting.cache/restore-cache!) (throw e)))) |
Get the value of a Setting that should be displayed to a User (i.e. via Accepts options:
| (defn user-facing-value [setting-definition-or-name & {:keys [getter], :or {getter get}}] (let [{:keys [sensitive? visibility default], k :name, :as setting} (resolve-setting setting-definition-or-name) unparsed-value (get-value-of-type :string k) parsed-value (getter k) ;; `default` and `env-var-value` are probably still in serialized form so compare value-is-default? (= parsed-value default) value-is-from-env-var? (some-> (env-var-value setting) (= unparsed-value))] (cond (not (current-user-can-access-setting? setting)) (throw (ex-info (tru "You do not have access to the setting {0}" k) setting)) ;; Settings set via an env var aren't returned for security purposes. (or value-is-default? value-is-from-env-var?) nil (= visibility :internal) (throw (Exception. (tru "Setting {0} is internal" k))) sensitive? (obfuscate-value parsed-value) :else parsed-value))) |
(defn- set-via-env-var? [setting] (some? (env-var-value setting))) | |
(defn- export? [setting-name] (:export? (core/get @registered-settings (keyword setting-name)))) | |
(defn- user-facing-info [{:keys [default description], k :name, :as setting} & {:as options}] (let [from-env? (set-via-env-var? setting)] {:key k :value (try (m/mapply user-facing-value setting options) (catch Throwable e (log/error e "Error fetching value of Setting"))) :is_env_setting from-env? :env_name (env-var-name setting) :description (str (description)) :default (if from-env? (tru "Using value of env var {0}" (str \$ (env-var-name setting))) default)})) | |
Returns a set of setting visibilities that the current user has read access to. | (defn current-user-readable-visibilities [] (set (concat [:public] (when @api/*current-user* [:authenticated]) (when (has-advanced-setting-access?) [:settings-manager]) (when api/*is-superuser?* [:admin])))) |
Returns a set of setting visibilities that the current user has write access to. | (defn current-user-writable-visibilities [] (set (concat [] (when (has-advanced-setting-access?) [:settings-manager :authenticated :public]) (when api/*is-superuser?* [:admin])))) |
Returns the user facing view of the registered settings satisfying the given predicate | (defn- user-facing-settings-matching [pred options] (into [] (comp (filter pred) (map #(m/mapply user-facing-info % options))) (sort-by :name (vals @registered-settings)))) |
Return a sequence of site-wide Settings maps in a format suitable for consumption by the frontend. (For security purposes, this doesn't return the value of a Setting if it was set via env var).
This is currently used by For settings managers who are not admins, only the subset of settings with the :settings-manager visibility level are returned. | (defn writable-settings [& {:as options}] ;; ignore Database-local values, but not User-local values (let [writable-visibilities (current-user-writable-visibilities)] (binding [*database-local-values* nil] (user-facing-settings-matching (fn [setting] (and (contains? writable-visibilities (:visibility setting)) (not= (:database-local setting) :only))) options)))) |
Returns a sequence of site-wide Settings maps, similar to [[writable-settings]]. However, this function excludes User-local Settings in addition to Database-local Settings. Settings that are optionally user-local will be included with their site-wide value, if a site-wide value is set.
This is used in [[metabase-enterprise.serialization.dump/dump-settings]] to serialize site-wide Settings. | (defn admin-writable-site-wide-settings [& {:as options}] ;; ignore User-local and Database-local values (binding [*user-local-values* (delay (atom nil)) *database-local-values* nil] (user-facing-settings-matching (fn [setting] (and (not= (:visibility setting) :internal) (allows-site-wide-values? setting))) options))) |
Returns true if a setting can be read according to the provided set of | (defn can-read-setting? [setting allowed-visibilities] (let [setting (resolve-setting setting)] (boolean (and (not (:sensitive? setting)) (contains? allowed-visibilities (:visibility setting)))))) |
Returns Settings as a map of setting name -> site-wide value for a given set of [[Visibility]] keywords
e.g. Settings marked This is currently used by | (defn user-readable-values-map [visibilities] ;; ignore Database-local values, but not User-local values (binding [*database-local-values* nil] (into {} (comp (filter (fn [[_setting-name setting]] (and (not (database-local-only? setting)) (can-read-setting? setting visibilities)))) (map (fn [[setting-name]] [setting-name (get setting-name)]))) @registered-settings))) |
Substitute an opaque exception to ensure no sensitive information in the raw value is exposed | (defn- redact-parse-ex [ex] (ex-info (trs "Error of type {0} thrown while parsing a setting" (type ex)) {:ex-type (type ex)})) |
Indicate whether we must redact an exception to avoid leaking sensitive env vars | (defmulti may-contain-raw-token? (fn [_ex setting] (:type setting))) |
fallback to redaction if we have not defined behaviour for a given format | (defmethod may-contain-raw-token? :default [_ _] false) |
(defmethod may-contain-raw-token? :boolean [_ _] false) | |
Non EOF exceptions will mention the next character | (defmethod may-contain-raw-token? :csv [ex _] (not (instance? java.io.EOFException ex))) |
Number parsing exceptions will quote the entire input | (defmethod may-contain-raw-token? :double [_ _] true) (defmethod may-contain-raw-token? :integer [_ _] true) (defmethod may-contain-raw-token? :positive-integer [_ _] true) |
Date parsing may quote the entire input, or a particular sub-portion, e.g. a misspelled month name | (defmethod may-contain-raw-token? :timestamp [_ _] true) |
Keyword parsing can never fail, but let's be paranoid | (defmethod may-contain-raw-token? :keyword [_ _] true) |
(defmethod may-contain-raw-token? :json [ex _] (cond (instance? JsonEOFException ex) false (instance? JsonParseException ex) true :else (do (log/warn ex "Unexpected exception while parsing JSON") ;; err on the side of caution true))) | |
(defn- redact-sensitive-tokens [ex raw-value] (if (may-contain-raw-token? ex raw-value) (redact-parse-ex ex) ex)) | |
Test whether the value configured for a given setting can be parsed as the expected type. Returns an map containing the exception if an issue is encountered, or nil if the value passes validation. | (defn- validate-setting [setting] (try (binding [*disable-init* true] (get-value-of-type (:type setting) setting)) nil (catch clojure.lang.ExceptionInfo e (let [parse-error (or (ex-cause e) e) parse-error (redact-sensitive-tokens parse-error setting) env-var? (set-via-env-var? setting)] (assoc (select-keys setting [:name :type]) :parse-error parse-error :env-var? env-var?))))) |
Check whether there are any issues with the format of application settings, e.g. an invalid JSON string. Note that this will only check settings whose [[defsetting]] forms have already been evaluated. | (defn validate-settings-formatting! [] (doseq [invalid-setting (keep validate-setting (vals @registered-settings))] (if (:env-var? invalid-setting) (throw (ex-info (trs "Invalid {0} configuration for setting: {1}" (u/upper-case-en (name (:type invalid-setting))) (name (:name invalid-setting))) (dissoc invalid-setting :parse-error) (:parse-error invalid-setting))) (log/warn (:parse-error invalid-setting) (format "Unable to parse setting %s" (:name invalid-setting)))))) |
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. | (defn migrate-encrypted-settings! [] ;; If we don't have an encryption key set, don't bother trying to decrypt anything. If stuff is encrypted in the DB, ;; we can't do anything about it (since we can't decrypt it). If stuff isn't decrypted in the DB, we have nothing to ;; do. (when (encryption/default-encryption-enabled?) (let [settings (filter prohibits-encryption? (vals @registered-settings))] (t2/with-transaction [_conn] (doseq [{v :value k :key} (t2/select :setting {:for :update :where [:and [:in :key (map setting-name settings)] ;; these are *definitely* decrypted already, let's not bother looking [:not [:in :value ["true" "false"]]]]}) :let [decrypted-v (encryption/maybe-decrypt v)] :when (not= decrypted-v v)] (t2/update! :setting :key k {:value decrypted-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)) | |