Skip to content
Snippets Groups Projects
Unverified Commit 1b821778 authored by Cam Saul's avatar Cam Saul Committed by GitHub
Browse files

Move `metabase.config.file` code => enterprise directory (#26108)

* Move metabase.config.file code => enterprise directory

* Test fix :wrench:

* Fix Kondo error

* Fix Kondo errors

* Remove unused namespace
parent 590759f8
No related branches found
No related tags found
No related merge requests found
Showing
with 521 additions and 442 deletions
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
metabase.plugins.initialize plugins.init metabase.plugins.initialize plugins.init
metabase.public-settings public-settings metabase.public-settings public-settings
metabase.public-settings.premium-features premium-features 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.markdown markdown
metabase.pulse.render render metabase.pulse.render render
metabase.pulse.render.body body metabase.pulse.render.body body
...@@ -581,9 +581,10 @@ ...@@ -581,9 +581,10 @@
metabase.test.data/run-mbql-query hooks.metabase.test.data/mbql-query 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-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.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/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-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-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-temp-file hooks.metabase.test.util/with-temp-file
metabase.test.util/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values metabase.test.util/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values
...@@ -597,6 +598,7 @@ ...@@ -597,6 +598,7 @@
metabase.test/with-column-remappings hooks.common/with-ignored-first-arg 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-group hooks.common/let-one-with-optional-value
metabase.test/with-log-level hooks.common/with-ignored-first-arg 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-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
metabase.test/with-temp* hooks.toucan.util.test/with-temp* metabase.test/with-temp* hooks.toucan.util.test/with-temp*
......
(ns ^{:added "0.45.0"} (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 "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. directory. See https://github.com/metabase/metabase/issues/2052 for more information.
...@@ -99,6 +99,9 @@ ...@@ -99,6 +99,9 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.walk :as walk] [clojure.walk :as walk]
[environ.core :as env] [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]
[metabase.driver.common.parameters.parse :as params.parse] [metabase.driver.common.parameters.parse :as params.parse]
[metabase.util :as u] [metabase.util :as u]
...@@ -106,32 +109,22 @@ ...@@ -106,32 +109,22 @@
[metabase.util.i18n :refer [trs]] [metabase.util.i18n :refer [trs]]
[yaml.core :as yaml])) [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) (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/def :metabase.config.file.config/config
(s/and (s/and
map? map?
(fn validate-section-configs [m] (fn validate-section-configs [m]
(doseq [[section-name section-config] 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)) (s/assert* spec section-config))
true))) true)))
...@@ -241,28 +234,14 @@ ...@@ -241,28 +234,14 @@
(s/assert* ::config m) (s/assert* ::config m)
(expand-templates 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! (defn ^{:added "0.45.0"} initialize!
"Initialize Metabase according to the directives in the config file, if it exists." "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)] (when-let [m (config)]
(doseq [[section-name section-config] (:config m)] (doseq [[section-name section-config] (:config m)]
(log/info (u/colorize :magenta (trs "Initializing {0} from config file..." section-name)) (u/emoji "🗄️")) (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 "🗄️"))) (log/info (u/colorize :magenta (trs "Done initializing from file.")) (u/emoji "🗄️")))
:ok) :ok)
(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)))
(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)))))
(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)))
(ns metabase.config.file-test (ns metabase-enterprise.config-from-file.core-test
(:require (:require
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer :all] [clojure.test :refer :all]
[clojure.walk :as walk] [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.test :as mt]
[metabase.util :as u] [metabase.util :as u]
[yaml.core :as yaml])) [yaml.core :as yaml]))
(use-fixtures :each (fn [thunk] (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)))) (thunk))))
(defn- re-quote [^String s] (defn- re-quote [^String s]
...@@ -23,46 +23,46 @@ ...@@ -23,46 +23,46 @@
(testing "Specify a custom path and read from YAML" (testing "Specify a custom path and read from YAML"
(mt/with-temp-file [filename "temp-config-file.yml"] (mt/with-temp-file [filename "temp-config-file.yml"]
(spit filename (yaml/generate-string mock-yaml)) (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 (is (= {:version 1
:config {:settings {:my-setting "abc123"}}} :config {:settings {:my-setting "abc123"}}}
(#'config.file/config)))))) (#'config-from-file/config))))))
(testing "Support overriding config with dynamic var for mocking purposes" (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 (is (= {:version 1
:config {:settings {:my-setting "abc123"}}} :config {:settings {:my-setting "abc123"}}}
(#'config.file/config)))))) (#'config-from-file/config))))))
(deftest ^:parallel validate-config-test (deftest ^:parallel validate-config-test
(testing "Config should throw an error" (testing "Config should throw an error"
(testing "if it is not a map" (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? (is (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
(re-quote "failed: map?") (re-quote "failed: map?")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "if version" (testing "if version"
(testing "is not included" (testing "is not included"
(binding [config.file/*config* {:config {:settings {}}}] (binding [config-from-file/*config* {:config {:settings {}}}]
(is (thrown-with-msg? (is (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
(re-quote "failed: (contains? % :version)") (re-quote "failed: (contains? % :version)")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "is unsupported" (testing "is unsupported"
(testing "because it is too old" (testing "because it is too old"
(binding [config.file/*supported-versions* {:min 2.0, :max 3.0} (binding [config-from-file/*supported-versions* {:min 2.0, :max 3.0}
config.file/*config* {:version 1.0, :config {}}] config-from-file/*config* {:version 1.0, :config {}}]
(is (thrown-with-msg? (is (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
(re-quote "failed: supported-version?") (re-quote "failed: supported-version?")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "because it is too new" (testing "because it is too new"
(binding [config.file/*supported-versions* {:min 2.0, :max 3.0} (binding [config-from-file/*supported-versions* {:min 2.0, :max 3.0}
config.file/*config* {:version 4.0, :config {}}] config-from-file/*config* {:version 4.0, :config {}}]
(is (thrown-with-msg? (is (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
(re-quote "failed: supported-version?") (re-quote "failed: supported-version?")
(#'config.file/config))))))))) (#'config-from-file/config)))))))))
(defn- mock-config-with-setting [s] (defn- mock-config-with-setting [s]
{:version 1.0, :config {:settings {:my-setting s}}}) {:version 1.0, :config {:settings {:my-setting s}}})
...@@ -70,8 +70,8 @@ ...@@ -70,8 +70,8 @@
(deftest ^:parallel expand-template-forms-test (deftest ^:parallel expand-template-forms-test
(testing "Ignore single curly brackets, or brackets with spaces between them" (testing "Ignore single curly brackets, or brackets with spaces between them"
(are [s] (= (mock-config-with-setting s) (are [s] (= (mock-config-with-setting s)
(binding [config.file/*config* (mock-config-with-setting s)] (binding [config-from-file/*config* (mock-config-with-setting s)]
(#'config.file/config))) (#'config-from-file/config)))
"{}}" "{}}"
"{}}" "{}}"
"{ {}}")) "{ {}}"))
...@@ -79,8 +79,8 @@ ...@@ -79,8 +79,8 @@
(are [template error-pattern] (thrown-with-msg? (are [template error-pattern] (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
error-pattern error-pattern
(binding [config.file/*config* (mock-config-with-setting template)] (binding [config-from-file/*config* (mock-config-with-setting template)]
(#'config.file/config))) (#'config-from-file/config)))
;; {{ without a corresponding }} ;; {{ without a corresponding }}
"{{}" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}") "{{}" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}")
"{{} }" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}") "{{} }" (re-quote "Invalid query: found [[ or {{ with no matching ]] or }}")
...@@ -93,36 +93,36 @@ ...@@ -93,36 +93,36 @@
(deftest ^:parallel recursive-template-form-expansion-test (deftest ^:parallel recursive-template-form-expansion-test
(testing "Recursive expansion is unsupported, for now." (testing "Recursive expansion is unsupported, for now."
(binding [config.file/*env* (assoc @#'config.file/*env* :x "{{env Y}}", :y "Y") (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :x "{{env Y}}", :y "Y")
config.file/*config* (mock-config-with-setting "{{env X}}")] config-from-file/*config* (mock-config-with-setting "{{env X}}")]
(is (= (mock-config-with-setting "{{env Y}}") (is (= (mock-config-with-setting "{{env Y}}")
(#'config.file/config)))))) (#'config-from-file/config))))))
(deftest ^:parallel expand-template-env-var-values-test (deftest ^:parallel expand-template-env-var-values-test
(testing "env var values" (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" (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") (is (= (mock-config-with-setting "Parrot Hilton")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "Should handle multiple templates in one string" (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") (is (= (mock-config-with-setting "Parrot Hilton-Parrot Hilton")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "Ignore whitespace inside the template brackets" (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") (is (= (mock-config-with-setting "Parrot Hilton")
(#'config.file/config))))) (#'config-from-file/config)))))
(testing "Ignore excess brackets" (testing "Ignore excess brackets"
(are [template expected] (= (mock-config-with-setting expected) (are [template expected] (= (mock-config-with-setting expected)
(binding [config.file/*config* (mock-config-with-setting template)] (binding [config-from-file/*config* (mock-config-with-setting template)]
(#'config.file/config))) (#'config-from-file/config)))
"{{{env CONFIG_FILE_BIRD_NAME}}" "{Parrot Hilton" "{{{env CONFIG_FILE_BIRD_NAME}}" "{Parrot Hilton"
"{{env CONFIG_FILE_BIRD_NAME}}}" "Parrot Hilton}")) "{{env CONFIG_FILE_BIRD_NAME}}}" "Parrot Hilton}"))
(testing "handle lisp-case/snake-case and case variations" (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") (is (= (mock-config-with-setting "Parrot Hilton")
(#'config.file/config)))))))) (#'config-from-file/config))))))))
(deftest ^:parallel expand-template-env-var-values-validation-test (deftest ^:parallel expand-template-env-var-values-validation-test
(testing "(config) should walk the config map and expand {{template}} forms" (testing "(config) should walk the config map and expand {{template}} forms"
...@@ -131,8 +131,8 @@ ...@@ -131,8 +131,8 @@
(are [template error-pattern] (thrown-with-msg? (are [template error-pattern] (thrown-with-msg?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
error-pattern error-pattern
(binding [config.file/*config* (mock-config-with-setting template)] (binding [config-from-file/*config* (mock-config-with-setting template)]
(#'config.file/config))) (#'config-from-file/config)))
;; missing env var name ;; missing env var name
"{{env}}" #"Insufficient input" "{{env}}" #"Insufficient input"
;; too many args ;; too many args
...@@ -142,51 +142,51 @@ ...@@ -142,51 +142,51 @@
(deftest ^:parallel optional-template-test (deftest ^:parallel optional-template-test
(testing "[[optional {{template}}]] values" (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" (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~~~") (is (= (mock-config-with-setting "~~~SeCrEt123~~~")
(#'config.file/config)))) (#'config-from-file/config))))
(binding [config.file/*config* (mock-config-with-setting "password__[[{{env MY_SENSITIVE_PASSWORD}}]]")] (binding [config-from-file/*config* (mock-config-with-setting "password__[[{{env MY_SENSITIVE_PASSWORD}}]]")]
(is (= (mock-config-with-setting "password__~~~SeCrEt123~~~") (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" (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") (is (= (mock-config-with-setting "before__~~~SeCrEt123~~~__after")
(#'config.file/config)))))) (#'config-from-file/config))))))
(testing "env var does not exist" (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 "") (is (= (mock-config-with-setting "")
(#'config.file/config)))) (#'config-from-file/config))))
(binding [config.file/*config* (mock-config-with-setting "password__[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")] (binding [config-from-file/*config* (mock-config-with-setting "password__[[{{env MY_OTHER_SENSITIVE_PASSWORD}}]]")]
(is (= (mock-config-with-setting "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" (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 "") (is (= (mock-config-with-setting "")
(#'config.file/config))))))))) (#'config-from-file/config)))))))))
(deftest initialize-section-test (deftest initialize-section-test
(testing "Ignore unknown sections" (testing "Ignore unknown sections"
(binding [config.file/*config* {:version 1.0, :config {:unknown-section {}}}] (binding [config-from-file/*config* {:version 1.0, :config {:unknown-section {}}}]
(let [log-messages (mt/with-log-messages-for-level [metabase.config.file :warn] (let [log-messages (mt/with-log-messages-for-level [metabase-enterprise.config-from-file.interface :warn]
(is (= :ok (is (= :ok
(config.file/initialize!))))] (config-from-file/initialize!))))]
(is (= [[:warn nil (u/colorize :yellow "Ignoring unknown config section :unknown-section.")]] (is (= [[:warn nil (u/colorize :yellow "Ignoring unknown config section :unknown-section.")]]
log-messages)))))) log-messages))))))
(deftest ^:parallel error-validation-do-not-leak-env-vars-test (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." (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~~~") (binding [config-from-file/*env* (assoc @#'config-from-file/*env* :my-sensitive-password "~~~SeCrEt123~~~")
config.file/*config* {:version 1 config-from-file/*config* {:version 1
:config {:users [{:first_name "Cam" :config {:users [{:first_name "Cam"
:last_name "Era" :last_name "Era"
:password "{{env MY_SENSITIVE_PASSWORD}}"}]}}] :password "{{env MY_SENSITIVE_PASSWORD}}"}]}}]
(is (thrown? (is (thrown?
clojure.lang.ExceptionInfo clojure.lang.ExceptionInfo
(#'config.file/config))) (#'config-from-file/config)))
(try (try
(#'config.file/config) (#'config-from-file/config)
(catch Throwable e (catch Throwable e
(letfn [(contains-password? [form] (letfn [(contains-password? [form]
(let [seen-password? (atom false)] (let [seen-password? (atom false)]
......
(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!)))))))
(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)")))))
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
[java-time :as t] [java-time :as t]
[metabase.analytics.prometheus :as prometheus] [metabase.analytics.prometheus :as prometheus]
[metabase.config :as config] [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.core.initialization-status :as init-status]
[metabase.db :as mdb] [metabase.db :as mdb]
metabase.driver.h2 metabase.driver.h2
...@@ -105,8 +105,8 @@ ...@@ -105,8 +105,8 @@
(log/info (trs "Setting up prometheus metrics")) (log/info (trs "Setting up prometheus metrics"))
(prometheus/setup!) (prometheus/setup!)
(init-status/set-progress! 0.6)) (init-status/set-progress! 0.6))
;; initialize Metabase from an `config.yml` file as needed. ;; initialize Metabase from an `config.yml` file if present (Enterprise Edition™ only)
(config.file/initialize!) (config-from-file/init-from-file-if-code-available!)
(init-status/set-progress! 0.65) (init-status/set-progress! 0.65)
;; Bootstrap the event system ;; Bootstrap the event system
(events/initialize-events!) (events/initialize-events!)
......
(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!))))
(ns metabase.models.database (ns metabase.models.database
(:require (:require
[cheshire.generate :refer [add-encoder encode-map]] [cheshire.generate :refer [add-encoder encode-map]]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[medley.core :as m] [medley.core :as m]
[metabase.config.file :as config.file]
[metabase.db.util :as mdb.u] [metabase.db.util :as mdb.u]
[metabase.driver :as driver] [metabase.driver :as driver]
[metabase.driver.impl :as driver.impl] [metabase.driver.impl :as driver.impl]
...@@ -314,42 +312,3 @@ ...@@ -314,42 +312,3 @@
(not (:details database)) (assoc :details "{}")) (not (:details database)) (assoc :details "{}"))
serdes.base/load-xform-basics serdes.base/load-xform-basics
(update :creator_id serdes.util/import-user))) (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)))
(ns metabase.models.user (ns metabase.models.user
(:require (:require
[clojure.data :as data] [clojure.data :as data]
[clojure.spec.alpha :as s]
[clojure.string :as str] [clojure.string :as str]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[metabase.config.file :as config.file]
[metabase.models.collection :as collection] [metabase.models.collection :as collection]
[metabase.models.permissions :as perms] [metabase.models.permissions :as perms]
[metabase.models.permissions-group :as perms-group] [metabase.models.permissions-group :as perms-group]
...@@ -367,56 +365,3 @@ ...@@ -367,56 +365,3 @@
(doseq [group-id to-add] (doseq [group-id to-add]
(db/insert! PermissionsGroupMembership {:user_id user-id, :group_id group-id})))) (db/insert! PermissionsGroupMembership {:user_id user-id, :group_id group-id}))))
true)) 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)))
(ns metabase.email.messages-test (ns metabase.email.messages-test
(:require [clojure.string :as str] (:require
[clojure.test :refer :all] [clojure.string :as str]
[metabase.email-test :as et] [clojure.test :refer :all]
[metabase.email.messages :as messages] [metabase.email-test :as et]
[metabase.test.util :as tu]) [metabase.email.messages :as messages]
(:import java.io.IOException)) [metabase.test :as mt]
[metabase.test.util :as tu])
(:import
(java.io IOException)))
(deftest new-user-email (deftest new-user-email
(is (= [{:from "notifications@metabase.com", (is (= [{:from "notifications@metabase.com",
...@@ -90,7 +93,7 @@ ...@@ -90,7 +93,7 @@
(deftest render-pulse-email-test (deftest render-pulse-email-test
(testing "Email with few rows and columns can be rendered when tracing (#21166)" (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" (let [result {:card {:name "card-name"
:visualization_settings :visualization_settings
{:table.column_formatting []}} {:table.column_formatting []}}
......
...@@ -4,11 +4,9 @@ ...@@ -4,11 +4,9 @@
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer :all] [clojure.test :refer :all]
[metabase.api.common :as api] [metabase.api.common :as api]
[metabase.config.file :as config.file]
[metabase.db.connection :as mdb.connection]
[metabase.driver :as driver] [metabase.driver :as driver]
[metabase.driver.util :as driver.u] [metabase.driver.util :as driver.u]
[metabase.models :refer [Database Permissions Table]] [metabase.models :refer [Database Permissions]]
[metabase.models.database :as database] [metabase.models.database :as database]
[metabase.models.interface :as mi] [metabase.models.interface :as mi]
[metabase.models.permissions :as perms] [metabase.models.permissions :as perms]
...@@ -263,46 +261,3 @@ ...@@ -263,46 +261,3 @@
(let [db (db/insert! Database (dissoc (mt/with-temp-defaults Database) :details))] (let [db (db/insert! Database (dissoc (mt/with-temp-defaults Database) :details))]
(is (partial= {:details {}} (is (partial= {:details {}}
db)))))) 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!)))))))
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
[clojure.set :as set] [clojure.set :as set]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer :all] [clojure.test :refer :all]
[metabase.config.file :as config.file]
[metabase.http-client :as client] [metabase.http-client :as client]
[metabase.models [metabase.models
:refer [Collection :refer [Collection
...@@ -477,132 +476,3 @@ ...@@ -477,132 +476,3 @@
(is (= "e8d63472" (is (= "e8d63472"
(serdes.hash/raw-hash ["fred@flintston.es"]) (serdes.hash/raw-hash ["fred@flintston.es"])
(serdes.hash/identity-hash user)))))) (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)")))))
(ns metabase.public-settings.premium-features-test (ns metabase.public-settings.premium-features-test
(:require [cheshire.core :as json] (:require
[clj-http.client :as http] [cheshire.core :as json]
[clj-http.fake :as http-fake] [clj-http.client :as http]
[clojure.test :refer :all] [clj-http.fake :as http-fake]
[metabase.config :as config] [clojure.test :refer :all]
[metabase.models.user :refer [User]] [metabase.config :as config]
[metabase.public-settings :as public-settings] [metabase.models.user :refer [User]]
[metabase.public-settings.premium-features :as premium-features :refer [defenterprise defenterprise-schema]] [metabase.public-settings :as public-settings]
[metabase.test :as mt] [metabase.public-settings.premium-features
[metabase.test.util :as tu] :as premium-features
[schema.core :as s] :refer [defenterprise defenterprise-schema]]
[toucan.util.test :as tt])) [metabase.test :as mt]
[schema.core :as s]
[toucan.util.test :as tt]))
(defn do-with-premium-features [features f] (defn do-with-premium-features [features f]
(let [features (set (map name features))] (let [features (set (map name features))]
...@@ -99,7 +101,7 @@ ...@@ -99,7 +101,7 @@
(is (contains? (set (:features result)) "test"))))))) (is (contains? (set (:features result)) "test")))))))
(deftest not-found-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 ;; `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 ;; 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 ;; for bug reports to come in
......
(ns metabase.test.util (ns metabase.test.util
"Helper functions and macros for writing unit tests." "Helper functions and macros for writing unit tests."
(:require [cheshire.core :as json] (:require
[clojure.java.io :as io] [cheshire.core :as json]
[clojure.set :as set] [clojure.java.io :as io]
[clojure.string :as str] [clojure.set :as set]
[clojure.test :refer :all] [clojure.string :as str]
[clojure.walk :as walk] [clojure.test :refer :all]
[clojurewerkz.quartzite.scheduler :as qs] [clojure.walk :as walk]
[colorize.core :as colorize] [clojurewerkz.quartzite.scheduler :as qs]
[environ.core :as env] [colorize.core :as colorize]
[java-time :as t] [environ.core :as env]
[metabase.models :refer [Card Collection Dashboard DashboardCardSeries Database Dimension Field FieldValues [java-time :as t]
LoginHistory Metric NativeQuerySnippet Permissions PermissionsGroup PermissionsGroupMembership [metabase.models
PersistedInfo Pulse PulseCard PulseChannel Revision Segment Setting :refer [Card
Table TaskHistory Timeline TimelineEvent User]] Collection
[metabase.models.collection :as collection] Dashboard
[metabase.models.interface :as mi] DashboardCardSeries
[metabase.models.permissions :as perms] Database
[metabase.models.permissions-group :as perms-group] Dimension
[metabase.models.setting :as setting] Field
[metabase.models.setting.cache :as setting.cache] FieldValues
[metabase.models.timeline :as timeline] LoginHistory
[metabase.plugins.classloader :as classloader] Metric
[metabase.task :as task] NativeQuerySnippet
[metabase.test-runner.assert-exprs :as test-runner.assert-exprs] Permissions
[metabase.test-runner.parallel :as test-runner.parallel] PermissionsGroup
[metabase.test.data :as data] PermissionsGroupMembership
[metabase.test.fixtures :as fixtures] PersistedInfo
[metabase.test.initialize :as initialize] Pulse
[metabase.test.util.log :as tu.log] PulseCard
[metabase.util :as u] PulseChannel
[metabase.util.files :as u.files] Revision
[potemkin :as p] Segment
[toucan.db :as db] Setting
[toucan.models :as models] Table
[toucan.util.test :as tt]) 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 (:import
(java.io File FileInputStream) (java.io File FileInputStream)
(java.net ServerSocket) (java.net ServerSocket)
...@@ -48,14 +71,6 @@ ...@@ -48,14 +71,6 @@
(use-fixtures :once (fixtures/initialize :db)) (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 [] (defn- random-uppercase-letter []
(char (+ (int \A) (rand-int 26)))) (char (+ (int \A) (rand-int 26))))
......
(ns metabase.util.encryption-test (ns metabase.util.encryption-test
"Tests for encryption of Metabase DB details." "Tests for encryption of Metabase DB details."
(:require [clojure.string :as str] (:require
[clojure.test :refer :all] [clojure.string :as str]
[metabase.models.setting.cache :as setting.cache] [clojure.test :refer :all]
[metabase.test.initialize :as initialize] [metabase.models.setting.cache :as setting.cache]
[metabase.test.util :as tu] [metabase.test :as mt]
[metabase.util.encryption :as encryption])) [metabase.test.initialize :as initialize]
[metabase.util.encryption :as encryption]))
(defn do-with-secret-key [^String secret-key thunk] (defn do-with-secret-key [^String secret-key thunk]
;; flush the Setting cache so unencrypted values have to be fetched from the DB again ;; flush the Setting cache so unencrypted values have to be fetched from the DB again
...@@ -88,7 +89,7 @@ ...@@ -88,7 +89,7 @@
(deftest no-errors-for-unencrypted-test (deftest no-errors-for-unencrypted-test
(testing "Something obviously not encrypted should avoiding trying to decrypt it (and thus not log an error)" (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")))))) (encryption/maybe-decrypt secret "abc"))))))
(def ^:private fake-ciphertext (def ^:private fake-ciphertext
...@@ -101,10 +102,10 @@ ...@@ -101,10 +102,10 @@
(testing (str "Something that is not encrypted, but might be (is the correct shape etc) should attempt to be " (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.") "decrypted. If unable to decrypt it, log a warning.")
(is (includes-encryption-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)))) (encryption/maybe-decrypt secret fake-ciphertext))))
(is (includes-encryption-warning? (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"))))))) (encryption/maybe-decrypt secret-2 (encryption/encrypt secret "WOW")))))))
(deftest ^:parallel possibly-encrypted-test (deftest ^:parallel possibly-encrypted-test
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment