diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 3449b7797399cb389c9ba0e3a91feb31961a9426..853a8d409d68a6097634b06496eba47c06246a5e 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -271,7 +271,7 @@ metabase.plugins.initialize plugins.init metabase.public-settings public-settings metabase.public-settings.premium-features premium-features - metabase.pulse pulse ; NB some conflicts with metabase.models.pulse + metabase.pulse pulse ; NB some conflicts with metabase.models.pulse metabase.pulse.markdown markdown metabase.pulse.render render metabase.pulse.render.body body @@ -581,9 +581,10 @@ metabase.test.data/run-mbql-query hooks.metabase.test.data/mbql-query metabase.test.util.async/with-chans hooks.common/let-with-optional-value-for-last-binding metabase.test.util.async/with-open-channels hooks.common/let-with-optional-value-for-last-binding + metabase.test.util.log/with-log-level hooks.common/with-ignored-first-arg + metabase.test.util.log/with-log-messages-for-level hooks.common/with-ignored-first-arg metabase.test.util/discard-setting-changes hooks.common/with-ignored-first-arg metabase.test.util/with-column-remappings hooks.common/with-ignored-first-arg - metabase.test.util/with-log-level hooks.common/with-ignored-first-arg metabase.test.util/with-non-admin-groups-no-root-collection-perms hooks.common/do* metabase.test.util/with-temp-file hooks.metabase.test.util/with-temp-file metabase.test.util/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values @@ -597,6 +598,7 @@ metabase.test/with-column-remappings hooks.common/with-ignored-first-arg metabase.test/with-group hooks.common/let-one-with-optional-value metabase.test/with-log-level hooks.common/with-ignored-first-arg + metabase.test/with-log-messages-for-level hooks.common/with-ignored-first-arg metabase.test/with-non-admin-groups-no-root-collection-perms hooks.common/do* metabase.test/with-temp hooks.toucan.util.test/with-temp metabase.test/with-temp* hooks.toucan.util.test/with-temp* diff --git a/src/metabase/config/file.clj b/enterprise/backend/src/metabase_enterprise/config_from_file/core.clj similarity index 85% rename from src/metabase/config/file.clj rename to enterprise/backend/src/metabase_enterprise/config_from_file/core.clj index 7a329cd453b2dbbd4da7c2b93d53fe1259c298d8..b363f6f36ab84010ca9541f92712dd60b91ac644 100644 --- a/src/metabase/config/file.clj +++ b/enterprise/backend/src/metabase_enterprise/config_from_file/core.clj @@ -1,5 +1,5 @@ (ns ^{:added "0.45.0"} - metabase.config.file + metabase-enterprise.config-from-file.core "Support for initializing Metabase with configuration from a `config.yml` file located in the current working directory. See https://github.com/metabase/metabase/issues/2052 for more information. @@ -99,6 +99,9 @@ [clojure.tools.logging :as log] [clojure.walk :as walk] [environ.core :as env] + [metabase-enterprise.config-from-file.databases] + [metabase-enterprise.config-from-file.interface :as config-from-file.i] + [metabase-enterprise.config-from-file.users] [metabase.driver.common.parameters] [metabase.driver.common.parameters.parse :as params.parse] [metabase.util :as u] @@ -106,32 +109,22 @@ [metabase.util.i18n :refer [trs]] [yaml.core :as yaml])) -(comment metabase.driver.common.parameters/keep-me) +(comment + ;; for parameter parsing + metabase.driver.common.parameters/keep-me + ;; for `:databases:` section code + metabase-enterprise.config-from-file.databases/keep-me + ;; for `users:` section code + metabase-enterprise.config-from-file.users/keep-me) (set! *warn-on-reflection* true) -(defmulti section-spec - "Spec that should be used to validate the config section with `section-name`, e.g. `:users`. Default spec - is [[any?]]. - - Sections are validated BEFORE template expansion, so as to avoid leaking any sensitive values in spec errors. Write - your specs accordingly! - - Implementations of this method live in other namespaces. For example, the section spec for the `:users` section - lives in [[metabase.models.user]]." - {:arglists '([section-name])} - keyword) - -(defmethod section-spec :default - [_section-name] - any?) - (s/def :metabase.config.file.config/config (s/and map? (fn validate-section-configs [m] (doseq [[section-name section-config] m - :let [spec (section-spec section-name)]] + :let [spec (config-from-file.i/section-spec section-name)]] (s/assert* spec section-config)) true))) @@ -241,28 +234,14 @@ (s/assert* ::config m) (expand-templates m))) -(defmulti initialize-section! - "Execute initialization code for the section of the init config file with the key `section-name` and value - `section-config`. - - Implementations of this method live in other namespaces, for example the method for the `:users` section (to - initialize Users) lives in [[metabase.models.user]]." - {:arglists '([section-name section-config])} - (fn [section-name _section-config] - (keyword section-name))) - -;;; if we don't know how to initialize a particular section, just log a warning and proceed. This way we can be -;;; forward-compatible and handle sections that might be unknown in a particular version of Metabase. -(defmethod initialize-section! :default - [section-name _section-config] - (log/warn (u/colorize :yellow (trs "Ignoring unknown config section {0}." (pr-str section-name))))) - (defn ^{:added "0.45.0"} initialize! "Initialize Metabase according to the directives in the config file, if it exists." [] + ;; TODO -- this should only do anything if we have an appropriate token (we should get a token for testing this before + ;; enabling that check tho) (when-let [m (config)] (doseq [[section-name section-config] (:config m)] (log/info (u/colorize :magenta (trs "Initializing {0} from config file..." section-name)) (u/emoji "🗄ï¸")) - (initialize-section! section-name section-config)) + (config-from-file.i/initialize-section! section-name section-config)) (log/info (u/colorize :magenta (trs "Done initializing from file.")) (u/emoji "🗄ï¸"))) :ok) diff --git a/enterprise/backend/src/metabase_enterprise/config_from_file/databases.clj b/enterprise/backend/src/metabase_enterprise/config_from_file/databases.clj new file mode 100644 index 0000000000000000000000000000000000000000..73bb69c176429e504b26315587c56e2a89b5bcc8 --- /dev/null +++ b/enterprise/backend/src/metabase_enterprise/config_from_file/databases.clj @@ -0,0 +1,46 @@ +(ns metabase-enterprise.config-from-file.databases + (:require + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log] + [metabase-enterprise.config-from-file.interface :as config-from-file.i] + [metabase.driver.util :as driver.u] + [metabase.models.database :refer [Database]] + [metabase.util :as u] + [metabase.util.i18n :refer [trs]] + [toucan.db :as db])) + +(s/def :metabase-enterprise.config-from-file.databases.config-file-spec/name + string?) + +(s/def :metabase-enterprise.config-from-file.databases.config-file-spec/engine + string?) + +(s/def :metabase-enterprise.config-from-file.databases.config-file-spec/details + map?) + +(s/def ::config-file-spec + (s/keys :req-un [:metabase-enterprise.config-from-file.databases.config-file-spec/engine + :metabase-enterprise.config-from-file.databases.config-file-spec/name + :metabase-enterprise.config-from-file.databases.config-file-spec/details])) + +(defmethod config-from-file.i/section-spec :databases + [_section] + (s/spec (s/* ::config-file-spec))) + +(defn- init-from-config-file! + [database] + ;; assert that we are able to connect to this Database. Otherwise, throw an Exception. + (driver.u/can-connect-with-details? (keyword (:engine database)) (:details database) :throw-exceptions) + (if-let [existing-database-id (db/select-one-id Database :engine (:engine database), :name (:name database))] + (do + (log/info (u/colorize :blue (trs "Updating Database {0} {1}" (:engine database) (pr-str (:name database))))) + (db/update! Database existing-database-id database)) + (do + (log/info (u/colorize :green (trs "Creating new {0} Database {1}" (:engine database) (pr-str (:name database))))) + (let [db (db/insert! Database database)] + ((requiring-resolve 'metabase.sync/sync-database!) db))))) + +(defmethod config-from-file.i/initialize-section! :databases + [_section-name databases] + (doseq [database databases] + (init-from-config-file! database))) diff --git a/enterprise/backend/src/metabase_enterprise/config_from_file/interface.clj b/enterprise/backend/src/metabase_enterprise/config_from_file/interface.clj new file mode 100644 index 0000000000000000000000000000000000000000..9fa276212401ca1e8aba09e70738258201ac1e8b --- /dev/null +++ b/enterprise/backend/src/metabase_enterprise/config_from_file/interface.clj @@ -0,0 +1,37 @@ +(ns metabase-enterprise.config-from-file.interface + (:require + [clojure.tools.logging :as log] + [metabase.util :as u] + [metabase.util.i18n :refer [trs]])) + +(defmulti section-spec + "Spec that should be used to validate the config section with `section-name`, e.g. `:users`. Default spec + is [[any?]]. + + Sections are validated BEFORE template expansion, so as to avoid leaking any sensitive values in spec errors. Write + your specs accordingly! + + Implementations of this method live in other namespaces. For example, the section spec for the `:users` section + lives in [[metabase.models.user]]." + {:arglists '([section-name])} + keyword) + +(defmethod section-spec :default + [_section-name] + any?) + +(defmulti initialize-section! + "Execute initialization code for the section of the init config file with the key `section-name` and value + `section-config`. + + Implementations of this method live in other namespaces, for example the method for the `:users` section (to + initialize Users) lives in [[metabase.models.user]]." + {:arglists '([section-name section-config])} + (fn [section-name _section-config] + (keyword section-name))) + +;;; if we don't know how to initialize a particular section, just log a warning and proceed. This way we can be +;;; forward-compatible and handle sections that might be unknown in a particular version of Metabase. +(defmethod initialize-section! :default + [section-name _section-config] + (log/warn (u/colorize :yellow (trs "Ignoring unknown config section {0}." (pr-str section-name))))) diff --git a/enterprise/backend/src/metabase_enterprise/config_from_file/users.clj b/enterprise/backend/src/metabase_enterprise/config_from_file/users.clj new file mode 100644 index 0000000000000000000000000000000000000000..a2fe138ab3caeeb632be4e7b2476b37c7af9fbcf --- /dev/null +++ b/enterprise/backend/src/metabase_enterprise/config_from_file/users.clj @@ -0,0 +1,59 @@ +(ns metabase-enterprise.config-from-file.users + (:require + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log] + [metabase-enterprise.config-from-file.interface :as config-from-file.i] + [metabase.models.user :refer [User]] + [metabase.util :as u] + [metabase.util.i18n :as i18n :refer [trs]] + [toucan.db :as db])) + +(s/def :metabase-enterprise.config-from-file.users.config-file-spec/first_name + string?) + +(s/def :metabase-enterprise.config-from-file.users.config-file-spec/last_name + string?) + +(s/def :metabase-enterprise.config-from-file.users.config-file-spec/password + string?) + +(s/def :metabase-enterprise.config-from-file.users.config-file-spec/email + string?) + +(s/def ::config-file-spec + (s/keys :req-un [:metabase-enterprise.config-from-file.users.config-file-spec/first_name + :metabase-enterprise.config-from-file.users.config-file-spec/last_name + :metabase-enterprise.config-from-file.users.config-file-spec/password + :metabase-enterprise.config-from-file.users.config-file-spec/email])) + +(defmethod config-from-file.i/section-spec :users + [_section] + (s/spec (s/* ::config-file-spec))) + +(defn- init-from-config-file-is-first-user? + "For [[init-from-config-file!]]: `true` if this the first User being created for this instance. If so, we will ALWAYS + create that User as a superuser, regardless of what is specified in the config file. (It doesn't make sense to + create the first User as anything other than a superuser)." + [] + (zero? (db/count User))) + +(defn- init-from-config-file! + [user] + ;; TODO -- if this is the FIRST user, we should probably make them a superuser, right? + (if-let [existing-user-id (db/select-one-id User :email (:email user))] + (do + (log/info (u/colorize :blue (trs "Updating User with email {0}" (pr-str (:email user))))) + (db/update! User existing-user-id user)) + ;; create a new user. If they are the first User, force them to be an admin. + (let [user (cond-> user + (init-from-config-file-is-first-user?) (assoc :is_superuser true))] + (log/info (u/colorize :green (trs "Creating the first User for this instance. The first user is always created as an admin."))) + (log/info (u/colorize :green (trs "Creating new User {0} with email {1}" + (pr-str (str (:first_name user) \space (:last_name user))) + (pr-str (:email user))))) + (db/insert! User user)))) + +(defmethod config-from-file.i/initialize-section! :users + [_section-name users] + (doseq [user users] + (init-from-config-file! user))) diff --git a/test/metabase/config/file_test.clj b/enterprise/backend/test/metabase_enterprise/config_from_file/core_test.clj similarity index 60% rename from test/metabase/config/file_test.clj rename to enterprise/backend/test/metabase_enterprise/config_from_file/core_test.clj index ca677ea12a5677083b34c54ec1446821be2a67d4..1cbecb6d9a9da95381f9653d114fe5191d81ae42 100644 --- a/test/metabase/config/file_test.clj +++ b/enterprise/backend/test/metabase_enterprise/config_from_file/core_test.clj @@ -1,15 +1,15 @@ -(ns metabase.config.file-test +(ns metabase-enterprise.config-from-file.core-test (:require [clojure.string :as str] [clojure.test :refer :all] [clojure.walk :as walk] - [metabase.config.file :as config.file] + [metabase-enterprise.config-from-file.core :as config-from-file] [metabase.test :as mt] [metabase.util :as u] [yaml.core :as yaml])) (use-fixtures :each (fn [thunk] - (binding [config.file/*supported-versions* {:min 1.0, :max 1.999}] + (binding [config-from-file/*supported-versions* {:min 1.0, :max 1.999}] (thunk)))) (defn- re-quote [^String s] @@ -23,46 +23,46 @@ (testing "Specify a custom path and read from YAML" (mt/with-temp-file [filename "temp-config-file.yml"] (spit filename (yaml/generate-string mock-yaml)) - (binding [config.file/*env* (assoc @#'config.file/*env* :mb-config-file-path filename)] + (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :mb-config-file-path filename)] (is (= {:version 1 :config {:settings {:my-setting "abc123"}}} - (#'config.file/config)))))) + (#'config-from-file/config)))))) (testing "Support overriding config with dynamic var for mocking purposes" - (binding [config.file/*config* mock-yaml] + (binding [config-from-file/*config* mock-yaml] (is (= {:version 1 :config {:settings {:my-setting "abc123"}}} - (#'config.file/config)))))) + (#'config-from-file/config)))))) (deftest ^:parallel validate-config-test (testing "Config should throw an error" (testing "if it is not a map" - (binding [config.file/*config* [1 2 3 4]] + (binding [config-from-file/*config* [1 2 3 4]] (is (thrown-with-msg? clojure.lang.ExceptionInfo (re-quote "failed: map?") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "if version" (testing "is not included" - (binding [config.file/*config* {:config {:settings {}}}] + (binding [config-from-file/*config* {:config {:settings {}}}] (is (thrown-with-msg? clojure.lang.ExceptionInfo (re-quote "failed: (contains? % :version)") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "is unsupported" (testing "because it is too old" - (binding [config.file/*supported-versions* {:min 2.0, :max 3.0} - config.file/*config* {:version 1.0, :config {}}] + (binding [config-from-file/*supported-versions* {:min 2.0, :max 3.0} + config-from-file/*config* {:version 1.0, :config {}}] (is (thrown-with-msg? clojure.lang.ExceptionInfo (re-quote "failed: supported-version?") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "because it is too new" - (binding [config.file/*supported-versions* {:min 2.0, :max 3.0} - config.file/*config* {:version 4.0, :config {}}] + (binding [config-from-file/*supported-versions* {:min 2.0, :max 3.0} + config-from-file/*config* {:version 4.0, :config {}}] (is (thrown-with-msg? clojure.lang.ExceptionInfo (re-quote "failed: supported-version?") - (#'config.file/config))))))))) + (#'config-from-file/config))))))))) (defn- mock-config-with-setting [s] {:version 1.0, :config {:settings {:my-setting s}}}) @@ -70,8 +70,8 @@ (deftest ^:parallel expand-template-forms-test (testing "Ignore single curly brackets, or brackets with spaces between them" (are [s] (= (mock-config-with-setting s) - (binding [config.file/*config* (mock-config-with-setting s)] - (#'config.file/config))) + (binding [config-from-file/*config* (mock-config-with-setting s)] + (#'config-from-file/config))) "{}}" "{}}" "{ {}}")) @@ -79,8 +79,8 @@ (are [template error-pattern] (thrown-with-msg? clojure.lang.ExceptionInfo error-pattern - (binding [config.file/*config* (mock-config-with-setting template)] - (#'config.file/config))) + (binding [config-from-file/*config* (mock-config-with-setting template)] + (#'config-from-file/config))) ;; {{ without a corresponding }} "{{}" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}") "{{} }" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}") @@ -93,36 +93,36 @@ (deftest ^:parallel recursive-template-form-expansion-test (testing "Recursive expansion is unsupported, for now." - (binding [config.file/*env* (assoc @#'config.file/*env* :x "{{env Y}}", :y "Y") - config.file/*config* (mock-config-with-setting "{{env X}}")] + (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :x "{{env Y}}", :y "Y") + config-from-file/*config* (mock-config-with-setting "{{env X}}")] (is (= (mock-config-with-setting "{{env Y}}") - (#'config.file/config)))))) + (#'config-from-file/config)))))) (deftest ^:parallel expand-template-env-var-values-test (testing "env var values" - (binding [config.file/*env* (assoc @#'config.file/*env* :config-file-bird-name "Parrot Hilton")] + (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :config-file-bird-name "Parrot Hilton")] (testing "Nothing weird" - (binding [config.file/*config* (mock-config-with-setting "{{env CONFIG_FILE_BIRD_NAME}}")] + (binding [config-from-file/*config* (mock-config-with-setting "{{env CONFIG_FILE_BIRD_NAME}}")] (is (= (mock-config-with-setting "Parrot Hilton") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "Should handle multiple templates in one string" - (binding [config.file/*config* (mock-config-with-setting "{{env CONFIG_FILE_BIRD_NAME}}-{{env CONFIG_FILE_BIRD_NAME}}")] + (binding [config-from-file/*config* (mock-config-with-setting "{{env CONFIG_FILE_BIRD_NAME}}-{{env CONFIG_FILE_BIRD_NAME}}")] (is (= (mock-config-with-setting "Parrot Hilton-Parrot Hilton") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "Ignore whitespace inside the template brackets" - (binding [config.file/*config* (mock-config-with-setting "{{ env CONFIG_FILE_BIRD_NAME }}")] + (binding [config-from-file/*config* (mock-config-with-setting "{{ env CONFIG_FILE_BIRD_NAME }}")] (is (= (mock-config-with-setting "Parrot Hilton") - (#'config.file/config))))) + (#'config-from-file/config))))) (testing "Ignore excess brackets" (are [template expected] (= (mock-config-with-setting expected) - (binding [config.file/*config* (mock-config-with-setting template)] - (#'config.file/config))) + (binding [config-from-file/*config* (mock-config-with-setting template)] + (#'config-from-file/config))) "{{{env CONFIG_FILE_BIRD_NAME}}" "{Parrot Hilton" "{{env CONFIG_FILE_BIRD_NAME}}}" "Parrot Hilton}")) (testing "handle lisp-case/snake-case and case variations" - (binding [config.file/*config* (mock-config-with-setting "{{env config-file-bird-name}}")] + (binding [config-from-file/*config* (mock-config-with-setting "{{env config-file-bird-name}}")] (is (= (mock-config-with-setting "Parrot Hilton") - (#'config.file/config)))))))) + (#'config-from-file/config)))))))) (deftest ^:parallel expand-template-env-var-values-validation-test (testing "(config) should walk the config map and expand {{template}} forms" @@ -131,8 +131,8 @@ (are [template error-pattern] (thrown-with-msg? clojure.lang.ExceptionInfo error-pattern - (binding [config.file/*config* (mock-config-with-setting template)] - (#'config.file/config))) + (binding [config-from-file/*config* (mock-config-with-setting template)] + (#'config-from-file/config))) ;; missing env var name "{{env}}" #"Insufficient input" ;; too many args @@ -142,51 +142,51 @@ (deftest ^:parallel optional-template-test (testing "[[optional {{template}}]] values" - (binding [config.file/*env* (assoc @#'config.file/*env* :my-sensitive-password "~~~SeCrEt123~~~")] + (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :my-sensitive-password "~~~SeCrEt123~~~")] (testing "env var exists" - (binding [config.file/*config* (mock-config-with-setting "[[{{env MY_SENSITIVE_PASSWORD}}]]")] + (binding [config-from-file/*config* (mock-config-with-setting "[[{{env MY_SENSITIVE_PASSWORD}}]]")] (is (= (mock-config-with-setting "~~~SeCrEt123~~~") - (#'config.file/config)))) - (binding [config.file/*config* (mock-config-with-setting "password__[[{{env MY_SENSITIVE_PASSWORD}}]]")] + (#'config-from-file/config)))) + (binding [config-from-file/*config* (mock-config-with-setting "password__[[{{env MY_SENSITIVE_PASSWORD}}]]")] (is (= (mock-config-with-setting "password__~~~SeCrEt123~~~") - (#'config.file/config)))) + (#'config-from-file/config)))) (testing "with text inside optional brackets before/after the templated part" - (binding [config.file/*config* (mock-config-with-setting "[[before__{{env MY_SENSITIVE_PASSWORD}}__after]]")] + (binding [config-from-file/*config* (mock-config-with-setting "[[before__{{env MY_SENSITIVE_PASSWORD}}__after]]")] (is (= (mock-config-with-setting "before__~~~SeCrEt123~~~__after") - (#'config.file/config)))))) + (#'config-from-file/config)))))) (testing "env var does not exist" - (binding [config.file/*config* (mock-config-with-setting "[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")] + (binding [config-from-file/*config* (mock-config-with-setting "[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")] (is (= (mock-config-with-setting "") - (#'config.file/config)))) - (binding [config.file/*config* (mock-config-with-setting "password__[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")] + (#'config-from-file/config)))) + (binding [config-from-file/*config* (mock-config-with-setting "password__[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")] (is (= (mock-config-with-setting "password__") - (#'config.file/config)))) + (#'config-from-file/config)))) (testing "with text inside optional brackets before/after the templated part" - (binding [config.file/*config* (mock-config-with-setting "[[before__{{env MY_OTHER_SENSITIVE_PASSWORD}}__after]]")] + (binding [config-from-file/*config* (mock-config-with-setting "[[before__{{env MY_OTHER_SENSITIVE_PASSWORD}}__after]]")] (is (= (mock-config-with-setting "") - (#'config.file/config))))))))) + (#'config-from-file/config))))))))) (deftest initialize-section-test (testing "Ignore unknown sections" - (binding [config.file/*config* {:version 1.0, :config {:unknown-section {}}}] - (let [log-messages (mt/with-log-messages-for-level [metabase.config.file :warn] + (binding [config-from-file/*config* {:version 1.0, :config {:unknown-section {}}}] + (let [log-messages (mt/with-log-messages-for-level [metabase-enterprise.config-from-file.interface :warn] (is (= :ok - (config.file/initialize!))))] + (config-from-file/initialize!))))] (is (= [[:warn nil (u/colorize :yellow "Ignoring unknown config section :unknown-section.")]] log-messages)))))) (deftest ^:parallel error-validation-do-not-leak-env-vars-test (testing "spec errors should not include contents of env vars -- expand templates after spec validation." - (binding [config.file/*env* (assoc @#'config.file/*env* :my-sensitive-password "~~~SeCrEt123~~~") - config.file/*config* {:version 1 + (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :my-sensitive-password "~~~SeCrEt123~~~") + config-from-file/*config* {:version 1 :config {:users [{:first_name "Cam" :last_name "Era" :password "{{env MY_SENSITIVE_PASSWORD}}"}]}}] (is (thrown? clojure.lang.ExceptionInfo - (#'config.file/config))) + (#'config-from-file/config))) (try - (#'config.file/config) + (#'config-from-file/config) (catch Throwable e (letfn [(contains-password? [form] (let [seen-password? (atom false)] diff --git a/enterprise/backend/test/metabase_enterprise/config_from_file/databases_test.clj b/enterprise/backend/test/metabase_enterprise/config_from_file/databases_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..07364f2db8034ef494917ff4b829ab1cff785a54 --- /dev/null +++ b/enterprise/backend/test/metabase_enterprise/config_from_file/databases_test.clj @@ -0,0 +1,52 @@ +(ns metabase-enterprise.config-from-file.databases-test + (:require + [clojure.test :refer :all] + [metabase-enterprise.config-from-file.core :as config-from-file] + [metabase.db.connection :as mdb.connection] + [metabase.models :refer [Database Table]] + [metabase.test :as mt] + [metabase.util :as u] + [toucan.db :as db])) + +(deftest init-from-config-file-test + (let [db-type (mdb.connection/db-type) + original-db (mt/with-driver db-type (mt/db))] + (try + (binding [config-from-file/*supported-versions* {:min 1, :max 1} + config-from-file/*config* {:version 1 + :config {:databases [{:name "init-from-config-file-test/test-data" + :engine (name db-type) + :details (:details original-db)}]}}] + (testing "Create a Database if it does not already exist" + (is (= :ok + (config-from-file/initialize!))) + (let [db (db/select-one Database :name "init-from-config-file-test/test-data")] + (is (partial= {:engine db-type} + db)) + (is (= 1 + (db/count Database :name "init-from-config-file-test/test-data"))) + (testing "do not duplicate if Database already exists" + (is (= :ok + (config-from-file/initialize!))) + (is (= 1 + (db/count Database :name "init-from-config-file-test/test-data"))) + (is (partial= {:engine db-type} + (db/select-one Database :name "init-from-config-file-test/test-data")))) + (testing "Database should have been synced" + (is (= (db/count Table :db_id (u/the-id original-db)) + (db/count Table :db_id (u/the-id db)))))))) + (finally + (db/delete! Database :name "init-from-config-file-test/test-data"))))) + +(deftest ^:parallel init-from-config-file-connection-validation-test + (testing "Validate connection details when creating a Database from a config file, and error if they are invalid" + (binding [config-from-file/*supported-versions* {:min 1, :max 1} + config-from-file/*config* {:version 1 + :config {:databases [{:name "inist-from-config-file-test/test-data-in-memory" + :engine "h2" + :details {:db "mem:some-in-memory-db"}}]}}] + (testing "Create a Database if it does not already exist" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Database cannot be found\." + (config-from-file/initialize!))))))) diff --git a/enterprise/backend/test/metabase_enterprise/config_from_file/users_test.clj b/enterprise/backend/test/metabase_enterprise/config_from_file/users_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..60fc6c8c76bb38dbffd694756ab9a15e628166f6 --- /dev/null +++ b/enterprise/backend/test/metabase_enterprise/config_from_file/users_test.clj @@ -0,0 +1,137 @@ +(ns metabase-enterprise.config-from-file.users-test + (:require + [clojure.test :refer :all] + [metabase-enterprise.config-from-file.core :as config-from-file] + [metabase-enterprise.config-from-file.users :as config-from-file.users] + [metabase.models :refer [User]] + [metabase.util.password :as u.password] + [toucan.db :as db])) + +(deftest init-from-config-file-test + (try + (binding [config-from-file/*supported-versions* {:min 1, :max 1} + config-from-file/*config* {:version 1 + :config {:users [{:first_name "Cam" + :last_name "Era" + :email "cam+config-file-test@metabase.com" + :password "2cans"}]}}] + (testing "Create a User if it does not already exist" + (is (= :ok + (config-from-file/initialize!))) + (is (partial= {:first_name "Cam" + :last_name "Era" + :email "cam+config-file-test@metabase.com"} + (db/select-one User :email "cam+config-file-test@metabase.com"))) + (is (= 1 + (db/count User :email "cam+config-file-test@metabase.com")))) + (testing "upsert if User already exists" + (binding [config-from-file/*config* {:version 1 + :config {:users [{:first_name "Cam" + :last_name "Saul" + :email "cam+config-file-test@metabase.com" + :password "2cans"}]}}] + (is (= :ok + (config-from-file/initialize!))) + (is (= 1 + (db/count User :email "cam+config-file-test@metabase.com"))) + (is (partial= {:first_name "Cam" + :last_name "Saul" + :email "cam+config-file-test@metabase.com"} + (db/select-one User :email "cam+config-file-test@metabase.com")))))) + (finally + (db/delete! User :email "cam+config-file-test@metabase.com")))) + +(deftest init-from-config-file-force-admin-for-first-user-test + (testing "If this is the first user being created, always make the user a superuser regardless of what is specified" + (try + (binding [config-from-file/*supported-versions* {:min 1, :max 1}] + (testing "Create the first User" + (binding [config-from-file/*config* {:version 1 + :config {:users [{:first_name "Cam" + :last_name "Era" + :email "cam+config-file-admin-test@metabase.com" + :password "2cans" + :is_superuser false}]}}] + (with-redefs [config-from-file.users/init-from-config-file-is-first-user? (constantly true)] + (is (= :ok + (config-from-file/initialize!))) + (is (partial= {:first_name "Cam" + :last_name "Era" + :email "cam+config-file-admin-test@metabase.com" + :is_superuser true} + (db/select-one User :email "cam+config-file-admin-test@metabase.com"))) + (is (= 1 + (db/count User :email "cam+config-file-admin-test@metabase.com")))))) + (testing "Create the another User, DO NOT force them to be an admin" + (binding [config-from-file/*config* {:version 1 + :config {:users [{:first_name "Cam" + :last_name "Saul" + :email "cam+config-file-admin-test-2@metabase.com" + :password "2cans" + :is_superuser false}]}}] + (is (= :ok + (config-from-file/initialize!))) + (is (partial= {:first_name "Cam" + :last_name "Saul" + :email "cam+config-file-admin-test-2@metabase.com" + :is_superuser false} + (db/select-one User :email "cam+config-file-admin-test-2@metabase.com"))) + (is (= 1 + (db/count User :email "cam+config-file-admin-test-2@metabase.com")))))) + (finally (db/delete! User :email [:in #{"cam+config-file-admin-test@metabase.com" + "cam+config-file-admin-test-2@metabase.com"}]))))) + +(deftest init-from-config-file-env-var-for-password-test + (testing "Ensure that we can set User password using {{env ...}} templates" + (try + (binding [config-from-file/*supported-versions* {:min 1, :max 1} + config-from-file/*config* {:version 1 + :config {:users [{:first_name "Cam" + :last_name "Era" + :email "cam+config-file-password-test@metabase.com" + :password "{{env USER_PASSWORD}}"}]}} + config-from-file/*env* (assoc @#'config-from-file/*env* :user-password "1234cans")] + (testing "Create a User if it does not already exist" + (is (= :ok + (config-from-file/initialize!))) + (let [user (db/select-one [User :first_name :last_name :email :password_salt :password] + :email "cam+config-file-password-test@metabase.com")] + (is (partial= {:first_name "Cam" + :last_name "Era" + :email "cam+config-file-password-test@metabase.com"} + user)) + (is (u.password/verify-password "1234cans" (:password_salt user) (:password user)))))) + (finally + (db/delete! User :email "cam+config-file-password-test@metabase.com"))))) + +(deftest ^:parallel init-from-config-file-validation-test + (binding [config-from-file/*supported-versions* {:min 1, :max 1}] + (are [user error-pattern] (thrown-with-msg? + clojure.lang.ExceptionInfo + error-pattern + (binding [config-from-file/*config* {:version 1 + :config {:users [user]}}] + (#'config-from-file/config))) + ;; missing email + {:first_name "Cam" + :last_name "Era" + :password "2cans"} + (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :email)")) + + ;; missing first name + {:last_name "Era" + :email "cam+config-file-admin-test@metabase.com" + :password "2cans"} + (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :first_name)")) + + ;; missing last name + {:first_name "Cam" + :email "cam+config-file-admin-test@metabase.com" + :password "2cans"} + (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :last_name)")) + + ;; missing password + {:first_name "Cam" + :last_name "Era" + :email "cam+config-file-test@metabase.com"} + (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :password)"))))) diff --git a/src/metabase/core.clj b/src/metabase/core.clj index 41dfb136e54453fd26622c36e4c813c2e389c74b..aab128843e94b60f3ddca6afa3ca73f66fbc822f 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -5,7 +5,7 @@ [java-time :as t] [metabase.analytics.prometheus :as prometheus] [metabase.config :as config] - [metabase.config.file :as config.file] + [metabase.core.config-from-file :as config-from-file] [metabase.core.initialization-status :as init-status] [metabase.db :as mdb] metabase.driver.h2 @@ -105,8 +105,8 @@ (log/info (trs "Setting up prometheus metrics")) (prometheus/setup!) (init-status/set-progress! 0.6)) - ;; initialize Metabase from an `config.yml` file as needed. - (config.file/initialize!) + ;; initialize Metabase from an `config.yml` file if present (Enterprise Editionâ„¢ only) + (config-from-file/init-from-file-if-code-available!) (init-status/set-progress! 0.65) ;; Bootstrap the event system (events/initialize-events!) diff --git a/src/metabase/core/config_from_file.clj b/src/metabase/core/config_from_file.clj new file mode 100644 index 0000000000000000000000000000000000000000..cc1840c9db40451066ac0e69ac9f83d382f3a609 --- /dev/null +++ b/src/metabase/core/config_from_file.clj @@ -0,0 +1,17 @@ +(ns metabase.core.config-from-file + (:require + [clojure.tools.logging :as log] + [metabase.plugins.classloader :as classloader])) + +(defn init-from-file-if-code-available! + "Shim for running the config-from-file code, used by [[metabase.core]]. The config-from-file code only ships in the + Enterprise Editionâ„¢ JAR, so this checks whether the namespace exists, and if it does, + invokes [[metabase-enterprise.config-from-file.core/initialize!]]; otherwise, this no-ops." + [] + (when (try + (classloader/require 'metabase-enterprise.config-from-file.core) + :ok + (catch Throwable _ + (log/debug "metabase-enterprise.config-from-file.core not available; cannot initialize from file.") + nil)) + ((resolve 'metabase-enterprise.config-from-file.core/initialize!)))) diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index c6a0cea13506d02655b214df41ddc83fec461cff..25a91ef7fa36198152e0c93c91a523171cf2c2cb 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -1,10 +1,8 @@ (ns metabase.models.database (:require [cheshire.generate :refer [add-encoder encode-map]] - [clojure.spec.alpha :as s] [clojure.tools.logging :as log] [medley.core :as m] - [metabase.config.file :as config.file] [metabase.db.util :as mdb.u] [metabase.driver :as driver] [metabase.driver.impl :as driver.impl] @@ -314,42 +312,3 @@ (not (:details database)) (assoc :details "{}")) serdes.base/load-xform-basics (update :creator_id serdes.util/import-user))) - - -;;;; initialization from files - -(s/def :metabase.models.database.config-file-spec/name - string?) - -(s/def :metabase.models.database.config-file-spec/engine - string?) - -(s/def :metabase.models.database.config-file-spec/details - map?) - -(s/def ::config-file-spec - (s/keys :req-un [:metabase.models.database.config-file-spec/engine - :metabase.models.database.config-file-spec/name - :metabase.models.database.config-file-spec/details])) - -(defmethod config.file/section-spec :databases - [_section] - (s/spec (s/* ::config-file-spec))) - -(defn- init-from-config-file! - [database] - ;; assert that we are able to connect to this Database. Otherwise, throw an Exception. - (driver.u/can-connect-with-details? (keyword (:engine database)) (:details database) :throw-exceptions) - (if-let [existing-database-id (db/select-one-id Database :engine (:engine database), :name (:name database))] - (do - (log/info (u/colorize :blue (trs "Updating Database {0} {1}" (:engine database) (pr-str (:name database))))) - (db/update! Database existing-database-id database)) - (do - (log/info (u/colorize :green (trs "Creating new {0} Database {1}" (:engine database) (pr-str (:name database))))) - (let [db (db/insert! Database database)] - ((requiring-resolve 'metabase.sync/sync-database!) db))))) - -(defmethod config.file/initialize-section! :databases - [_section-name databases] - (doseq [database databases] - (init-from-config-file! database))) diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index b423dab5f8002140b175d65128bd666c065fd2e8..1c7f85572ac2a29f27a821c601887c8ad00495a8 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -1,10 +1,8 @@ (ns metabase.models.user (:require [clojure.data :as data] - [clojure.spec.alpha :as s] [clojure.string :as str] [clojure.tools.logging :as log] - [metabase.config.file :as config.file] [metabase.models.collection :as collection] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] @@ -367,56 +365,3 @@ (doseq [group-id to-add] (db/insert! PermissionsGroupMembership {:user_id user-id, :group_id group-id})))) true)) - - -;;;; initialization from files - -(s/def :metabase.models.user.config-file-spec/first_name - string?) - -(s/def :metabase.models.user.config-file-spec/last_name - string?) - -(s/def :metabase.models.user.config-file-spec/password - string?) - -(s/def :metabase.models.user.config-file-spec/email - string?) - -(s/def ::config-file-spec - (s/keys :req-un [:metabase.models.user.config-file-spec/first_name - :metabase.models.user.config-file-spec/last_name - :metabase.models.user.config-file-spec/password - :metabase.models.user.config-file-spec/email])) - -(defmethod config.file/section-spec :users - [_section] - (s/spec (s/* ::config-file-spec))) - -(defn- init-from-config-file-is-first-user? - "For [[init-from-config-file!]]: `true` if this the first User being created for this instance. If so, we will ALWAYS - create that User as a superuser, regardless of what is specified in the config file. (It doesn't make sense to - create the first User as anything other than a superuser)." - [] - (zero? (db/count User))) - -(defn- init-from-config-file! - [user] - ;; TODO -- if this is the FIRST user, we should probably make them a superuser, right? - (if-let [existing-user-id (db/select-one-id User :email (:email user))] - (do - (log/info (u/colorize :blue (trs "Updating User with email {0}" (pr-str (:email user))))) - (db/update! User existing-user-id user)) - ;; create a new user. If they are the first User, force them to be an admin. - (let [user (cond-> user - (init-from-config-file-is-first-user?) (assoc :is_superuser true))] - (log/info (u/colorize :green (trs "Creating the first User for this instance. The first user is always created as an admin."))) - (log/info (u/colorize :green (trs "Creating new User {0} with email {1}" - (pr-str (str (:first_name user) \space (:last_name user))) - (pr-str (:email user))))) - (db/insert! User user)))) - -(defmethod config.file/initialize-section! :users - [_section-name users] - (doseq [user users] - (init-from-config-file! user))) diff --git a/test/metabase/email/messages_test.clj b/test/metabase/email/messages_test.clj index 2f513fee0dde0d5ad260b583aaf36aaf5d3f89b4..41b5c45ac5d8c15f6cbbf90c103ad4b557f10606 100644 --- a/test/metabase/email/messages_test.clj +++ b/test/metabase/email/messages_test.clj @@ -1,10 +1,13 @@ (ns metabase.email.messages-test - (:require [clojure.string :as str] - [clojure.test :refer :all] - [metabase.email-test :as et] - [metabase.email.messages :as messages] - [metabase.test.util :as tu]) - (:import java.io.IOException)) + (:require + [clojure.string :as str] + [clojure.test :refer :all] + [metabase.email-test :as et] + [metabase.email.messages :as messages] + [metabase.test :as mt] + [metabase.test.util :as tu]) + (:import + (java.io IOException))) (deftest new-user-email (is (= [{:from "notifications@metabase.com", @@ -90,7 +93,7 @@ (deftest render-pulse-email-test (testing "Email with few rows and columns can be rendered when tracing (#21166)" - (tu/with-log-level [metabase.email :trace] + (mt/with-log-level [metabase.email :trace] (let [result {:card {:name "card-name" :visualization_settings {:table.column_formatting []}} diff --git a/test/metabase/models/database_test.clj b/test/metabase/models/database_test.clj index 8d0e390f6db6f776c927c3132c20b697a576e4ce..0dd088560db10fbcc72b3da5c3bff09d07769862 100644 --- a/test/metabase/models/database_test.clj +++ b/test/metabase/models/database_test.clj @@ -4,11 +4,9 @@ [clojure.string :as str] [clojure.test :refer :all] [metabase.api.common :as api] - [metabase.config.file :as config.file] - [metabase.db.connection :as mdb.connection] [metabase.driver :as driver] [metabase.driver.util :as driver.u] - [metabase.models :refer [Database Permissions Table]] + [metabase.models :refer [Database Permissions]] [metabase.models.database :as database] [metabase.models.interface :as mi] [metabase.models.permissions :as perms] @@ -263,46 +261,3 @@ (let [db (db/insert! Database (dissoc (mt/with-temp-defaults Database) :details))] (is (partial= {:details {}} db)))))) - -(deftest init-from-config-file-test - (let [db-type (mdb.connection/db-type) - original-db (mt/with-driver db-type (mt/db))] - (try - (binding [config.file/*supported-versions* {:min 1, :max 1} - config.file/*config* {:version 1 - :config {:databases [{:name "init-from-config-file-test/test-data" - :engine (name db-type) - :details (:details original-db)}]}}] - (testing "Create a Database if it does not already exist" - (is (= :ok - (config.file/initialize!))) - (let [db (db/select-one Database :name "init-from-config-file-test/test-data")] - (is (partial= {:engine db-type} - db)) - (is (= 1 - (db/count Database :name "init-from-config-file-test/test-data"))) - (testing "do not duplicate if Database already exists" - (is (= :ok - (config.file/initialize!))) - (is (= 1 - (db/count Database :name "init-from-config-file-test/test-data"))) - (is (partial= {:engine db-type} - (db/select-one Database :name "init-from-config-file-test/test-data")))) - (testing "Database should have been synced" - (is (= (db/count Table :db_id (u/the-id original-db)) - (db/count Table :db_id (u/the-id db)))))))) - (finally - (db/delete! Database :name "init-from-config-file-test/test-data"))))) - -(deftest ^:parallel init-from-config-file-connection-validation-test - (testing "Validate connection details when creating a Database from a config file, and error if they are invalid" - (binding [config.file/*supported-versions* {:min 1, :max 1} - config.file/*config* {:version 1 - :config {:databases [{:name "inist-from-config-file-test/test-data-in-memory" - :engine "h2" - :details {:db "mem:some-in-memory-db"}}]}}] - (testing "Create a Database if it does not already exist" - (is (thrown-with-msg? - clojure.lang.ExceptionInfo - #"Database cannot be found\." - (config.file/initialize!))))))) diff --git a/test/metabase/models/user_test.clj b/test/metabase/models/user_test.clj index 3ed3494a05fa2e34e9b40e0378416ff8e4f86ecf..b2eb0b6f4e0f12907c7ceaab1007ef8f4a4733cf 100644 --- a/test/metabase/models/user_test.clj +++ b/test/metabase/models/user_test.clj @@ -3,7 +3,6 @@ [clojure.set :as set] [clojure.string :as str] [clojure.test :refer :all] - [metabase.config.file :as config.file] [metabase.http-client :as client] [metabase.models :refer [Collection @@ -477,132 +476,3 @@ (is (= "e8d63472" (serdes.hash/raw-hash ["fred@flintston.es"]) (serdes.hash/identity-hash user)))))) - -(deftest init-from-config-file-test - (try - (binding [config.file/*supported-versions* {:min 1, :max 1} - config.file/*config* {:version 1 - :config {:users [{:first_name "Cam" - :last_name "Era" - :email "cam+config-file-test@metabase.com" - :password "2cans"}]}}] - (testing "Create a User if it does not already exist" - (is (= :ok - (config.file/initialize!))) - (is (partial= {:first_name "Cam" - :last_name "Era" - :email "cam+config-file-test@metabase.com"} - (db/select-one User :email "cam+config-file-test@metabase.com"))) - (is (= 1 - (db/count User :email "cam+config-file-test@metabase.com")))) - (testing "upsert if User already exists" - (binding [config.file/*config* {:version 1 - :config {:users [{:first_name "Cam" - :last_name "Saul" - :email "cam+config-file-test@metabase.com" - :password "2cans"}]}}] - (is (= :ok - (config.file/initialize!))) - (is (= 1 - (db/count User :email "cam+config-file-test@metabase.com"))) - (is (partial= {:first_name "Cam" - :last_name "Saul" - :email "cam+config-file-test@metabase.com"} - (db/select-one User :email "cam+config-file-test@metabase.com")))))) - (finally - (db/delete! User :email "cam+config-file-test@metabase.com")))) - -(deftest init-from-config-file-force-admin-for-first-user-test - (testing "If this is the first user being created, always make the user a superuser regardless of what is specified" - (try - (binding [config.file/*supported-versions* {:min 1, :max 1}] - (testing "Create the first User" - (binding [config.file/*config* {:version 1 - :config {:users [{:first_name "Cam" - :last_name "Era" - :email "cam+config-file-admin-test@metabase.com" - :password "2cans" - :is_superuser false}]}}] - (with-redefs [user/init-from-config-file-is-first-user? (constantly true)] - (is (= :ok - (config.file/initialize!))) - (is (partial= {:first_name "Cam" - :last_name "Era" - :email "cam+config-file-admin-test@metabase.com" - :is_superuser true} - (db/select-one User :email "cam+config-file-admin-test@metabase.com"))) - (is (= 1 - (db/count User :email "cam+config-file-admin-test@metabase.com")))))) - (testing "Create the another User, DO NOT force them to be an admin" - (binding [config.file/*config* {:version 1 - :config {:users [{:first_name "Cam" - :last_name "Saul" - :email "cam+config-file-admin-test-2@metabase.com" - :password "2cans" - :is_superuser false}]}}] - (is (= :ok - (config.file/initialize!))) - (is (partial= {:first_name "Cam" - :last_name "Saul" - :email "cam+config-file-admin-test-2@metabase.com" - :is_superuser false} - (db/select-one User :email "cam+config-file-admin-test-2@metabase.com"))) - (is (= 1 - (db/count User :email "cam+config-file-admin-test-2@metabase.com")))))) - (finally (db/delete! User :email [:in #{"cam+config-file-admin-test@metabase.com" - "cam+config-file-admin-test-2@metabase.com"}]))))) - -(deftest init-from-config-file-env-var-for-password-test - (testing "Ensure that we can set User password using {{env ...}} templates" - (try - (binding [config.file/*supported-versions* {:min 1, :max 1} - config.file/*config* {:version 1 - :config {:users [{:first_name "Cam" - :last_name "Era" - :email "cam+config-file-password-test@metabase.com" - :password "{{env USER_PASSWORD}}"}]}} - config.file/*env* (assoc @#'config.file/*env* :user-password "1234cans")] - (testing "Create a User if it does not already exist" - (is (= :ok - (config.file/initialize!))) - (let [user (db/select-one [User :first_name :last_name :email :password_salt :password] - :email "cam+config-file-password-test@metabase.com")] - (is (partial= {:first_name "Cam" - :last_name "Era" - :email "cam+config-file-password-test@metabase.com"} - user)) - (is (u.password/verify-password "1234cans" (:password_salt user) (:password user)))))) - (finally - (db/delete! User :email "cam+config-file-password-test@metabase.com"))))) - -(deftest ^:parallel init-from-config-file-validation-test - (binding [config.file/*supported-versions* {:min 1, :max 1}] - (are [user error-pattern] (thrown-with-msg? - clojure.lang.ExceptionInfo - error-pattern - (binding [config.file/*config* {:version 1 - :config {:users [user]}}] - (#'config.file/config))) - ;; missing email - {:first_name "Cam" - :last_name "Era" - :password "2cans"} - (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :email)")) - - ;; missing first name - {:last_name "Era" - :email "cam+config-file-admin-test@metabase.com" - :password "2cans"} - (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :first_name)")) - - ;; missing last name - {:first_name "Cam" - :email "cam+config-file-admin-test@metabase.com" - :password "2cans"} - (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :last_name)")) - - ;; missing password - {:first_name "Cam" - :last_name "Era" - :email "cam+config-file-test@metabase.com"} - (re-pattern (java.util.regex.Pattern/quote "failed: (contains? % :password)"))))) diff --git a/test/metabase/public_settings/premium_features_test.clj b/test/metabase/public_settings/premium_features_test.clj index 18148289d9d97da2c699dd158e7c8009859c10a7..6bb1a28178c3119646a87d2571e797481f052622 100644 --- a/test/metabase/public_settings/premium_features_test.clj +++ b/test/metabase/public_settings/premium_features_test.clj @@ -1,16 +1,18 @@ (ns metabase.public-settings.premium-features-test - (:require [cheshire.core :as json] - [clj-http.client :as http] - [clj-http.fake :as http-fake] - [clojure.test :refer :all] - [metabase.config :as config] - [metabase.models.user :refer [User]] - [metabase.public-settings :as public-settings] - [metabase.public-settings.premium-features :as premium-features :refer [defenterprise defenterprise-schema]] - [metabase.test :as mt] - [metabase.test.util :as tu] - [schema.core :as s] - [toucan.util.test :as tt])) + (:require + [cheshire.core :as json] + [clj-http.client :as http] + [clj-http.fake :as http-fake] + [clojure.test :refer :all] + [metabase.config :as config] + [metabase.models.user :refer [User]] + [metabase.public-settings :as public-settings] + [metabase.public-settings.premium-features + :as premium-features + :refer [defenterprise defenterprise-schema]] + [metabase.test :as mt] + [schema.core :as s] + [toucan.util.test :as tt])) (defn do-with-premium-features [features f] (let [features (set (map name features))] @@ -99,7 +101,7 @@ (is (contains? (set (:features result)) "test"))))))) (deftest not-found-test - (tu/with-log-level :fatal + (mt/with-log-level :fatal ;; `partial=` here in case the Cloud API starts including extra keys... this is a "dangerous" test since changes ;; upstream in Cloud could break this. We probably want to catch that stuff anyway tho in tests rather than waiting ;; for bug reports to come in diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index 654a43600c6d1a8a70e3f3026e62e43accffe9e9..fa30382b6922cd2db34708b789c98915718f6fd7 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -1,40 +1,63 @@ (ns metabase.test.util "Helper functions and macros for writing unit tests." - (:require [cheshire.core :as json] - [clojure.java.io :as io] - [clojure.set :as set] - [clojure.string :as str] - [clojure.test :refer :all] - [clojure.walk :as walk] - [clojurewerkz.quartzite.scheduler :as qs] - [colorize.core :as colorize] - [environ.core :as env] - [java-time :as t] - [metabase.models :refer [Card Collection Dashboard DashboardCardSeries Database Dimension Field FieldValues - LoginHistory Metric NativeQuerySnippet Permissions PermissionsGroup PermissionsGroupMembership - PersistedInfo Pulse PulseCard PulseChannel Revision Segment Setting - Table TaskHistory Timeline TimelineEvent User]] - [metabase.models.collection :as collection] - [metabase.models.interface :as mi] - [metabase.models.permissions :as perms] - [metabase.models.permissions-group :as perms-group] - [metabase.models.setting :as setting] - [metabase.models.setting.cache :as setting.cache] - [metabase.models.timeline :as timeline] - [metabase.plugins.classloader :as classloader] - [metabase.task :as task] - [metabase.test-runner.assert-exprs :as test-runner.assert-exprs] - [metabase.test-runner.parallel :as test-runner.parallel] - [metabase.test.data :as data] - [metabase.test.fixtures :as fixtures] - [metabase.test.initialize :as initialize] - [metabase.test.util.log :as tu.log] - [metabase.util :as u] - [metabase.util.files :as u.files] - [potemkin :as p] - [toucan.db :as db] - [toucan.models :as models] - [toucan.util.test :as tt]) + (:require + [cheshire.core :as json] + [clojure.java.io :as io] + [clojure.set :as set] + [clojure.string :as str] + [clojure.test :refer :all] + [clojure.walk :as walk] + [clojurewerkz.quartzite.scheduler :as qs] + [colorize.core :as colorize] + [environ.core :as env] + [java-time :as t] + [metabase.models + :refer [Card + Collection + Dashboard + DashboardCardSeries + Database + Dimension + Field + FieldValues + LoginHistory + Metric + NativeQuerySnippet + Permissions + PermissionsGroup + PermissionsGroupMembership + PersistedInfo + Pulse + PulseCard + PulseChannel + Revision + Segment + Setting + Table + TaskHistory + Timeline + TimelineEvent + User]] + [metabase.models.collection :as collection] + [metabase.models.interface :as mi] + [metabase.models.permissions :as perms] + [metabase.models.permissions-group :as perms-group] + [metabase.models.setting :as setting] + [metabase.models.setting.cache :as setting.cache] + [metabase.models.timeline :as timeline] + [metabase.plugins.classloader :as classloader] + [metabase.task :as task] + [metabase.test-runner.assert-exprs :as test-runner.assert-exprs] + [metabase.test-runner.parallel :as test-runner.parallel] + [metabase.test.data :as data] + [metabase.test.fixtures :as fixtures] + [metabase.test.initialize :as initialize] + [metabase.test.util.log :as tu.log] + [metabase.util :as u] + [metabase.util.files :as u.files] + [toucan.db :as db] + [toucan.models :as models] + [toucan.util.test :as tt]) (:import (java.io File FileInputStream) (java.net ServerSocket) @@ -48,14 +71,6 @@ (use-fixtures :once (fixtures/initialize :db)) -;; these are imported because these functions originally lived in this namespace, and some tests might still be -;; referencing them from here. We can remove the imports once everyone is using `metabase.test` instead of using this -;; namespace directly. -(p/import-vars - [tu.log - with-log-level - with-log-messages-for-level]) - (defn- random-uppercase-letter [] (char (+ (int \A) (rand-int 26)))) diff --git a/test/metabase/util/encryption_test.clj b/test/metabase/util/encryption_test.clj index c4163e9c3c57daa1bc5847cb6c78664e3f0e722b..2a557d0af25de1fde6c9e7ad34dfe7da7be7f683 100644 --- a/test/metabase/util/encryption_test.clj +++ b/test/metabase/util/encryption_test.clj @@ -1,11 +1,12 @@ (ns metabase.util.encryption-test "Tests for encryption of Metabase DB details." - (:require [clojure.string :as str] - [clojure.test :refer :all] - [metabase.models.setting.cache :as setting.cache] - [metabase.test.initialize :as initialize] - [metabase.test.util :as tu] - [metabase.util.encryption :as encryption])) + (:require + [clojure.string :as str] + [clojure.test :refer :all] + [metabase.models.setting.cache :as setting.cache] + [metabase.test :as mt] + [metabase.test.initialize :as initialize] + [metabase.util.encryption :as encryption])) (defn do-with-secret-key [^String secret-key thunk] ;; flush the Setting cache so unencrypted values have to be fetched from the DB again @@ -88,7 +89,7 @@ (deftest no-errors-for-unencrypted-test (testing "Something obviously not encrypted should avoiding trying to decrypt it (and thus not log an error)" - (is (empty? (tu/with-log-messages-for-level :warn + (is (empty? (mt/with-log-messages-for-level :warn (encryption/maybe-decrypt secret "abc")))))) (def ^:private fake-ciphertext @@ -101,10 +102,10 @@ (testing (str "Something that is not encrypted, but might be (is the correct shape etc) should attempt to be " "decrypted. If unable to decrypt it, log a warning.") (is (includes-encryption-warning? - (tu/with-log-messages-for-level :warn + (mt/with-log-messages-for-level :warn (encryption/maybe-decrypt secret fake-ciphertext)))) (is (includes-encryption-warning? - (tu/with-log-messages-for-level :warn + (mt/with-log-messages-for-level :warn (encryption/maybe-decrypt secret-2 (encryption/encrypt secret "WOW"))))))) (deftest ^:parallel possibly-encrypted-test