diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj index fde4c21c1a9de13bae64f20527f8486f6ddb64e3..d4b95a6bfe28d922e70ced55ac95cda9c55b5365 100644 --- a/src/metabase/api/routes.clj +++ b/src/metabase/api/routes.clj @@ -65,7 +65,7 @@ (defroutes ^{:doc "Ring routes for API endpoints."} routes ee-routes - (context "/action" [] (+auth api.action/routes)) + (context "/action" [] (+auth api.action/routes)) (context "/activity" [] (+auth api.activity/routes)) (context "/alert" [] (+auth api.alert/routes)) (context "/automagic-dashboards" [] (+auth api.magic/routes)) diff --git a/src/metabase/api/routes/common.clj b/src/metabase/api/routes/common.clj index 6bb5f7578b09a0835d10cfc019162c66964c4771..c9a03c785c289ab71b6cc93bf2917e92db09a774 100644 --- a/src/metabase/api/routes/common.clj +++ b/src/metabase/api/routes/common.clj @@ -8,17 +8,17 @@ (def +generic-exceptions "Wrap `routes` so any Exception thrown is just returned as a generic 400, to prevent details from leaking in public endpoints." - mw.exceptions/genericize-exceptions) + #'mw.exceptions/genericize-exceptions) (def +message-only-exceptions "Wrap `routes` so any Exception thrown is just returned as a 400 with only the message from the original Exception (i.e., remove the original stacktrace), to prevent details from leaking in public endpoints." - mw.exceptions/message-only-exceptions) + #'mw.exceptions/message-only-exceptions) (def +apikey "Wrap `routes` so they may only be accessed with a correct API key header." - mw.auth/enforce-api-key) + #'mw.auth/enforce-api-key) (def +auth "Wrap `routes` so they may only be accessed with proper authentication credentials." - mw.auth/enforce-authentication) + #'mw.auth/enforce-authentication) diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 94295e0e8c39a7e9e43b6fc6b3f0555b641b6960..022c1774b8d17d8f57589b8a619d618114396a08 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -398,6 +398,11 @@ (str/replace "-" "_") str/upper-case))) +(defn setting-env-map-name + "Correctly translate a setting to the keyword it will be found at in [[env/env]]." + [setting-definition-or-name] + (keyword (str "mb-" (munge-setting-name (setting-name setting-definition-or-name))))) + (defn env-var-value "Get the value of `setting-definition-or-name` from the corresponding env var, if any. The name of the Setting is converted to uppercase and dashes to underscores; for example, a setting named @@ -407,7 +412,7 @@ ^String [setting-definition-or-name] (let [setting (resolve-setting setting-definition-or-name)] (when (allows-site-wide-values? setting) - (let [v (env/env (keyword (str "mb-" (munge-setting-name (setting-name setting)))))] + (let [v (env/env (setting-env-map-name setting))] (when (seq v) v))))) diff --git a/src/metabase/server/middleware/auth.clj b/src/metabase/server/middleware/auth.clj index 509f3f402388afb772299370952da3499887fad3..d3928d701d9e802d8e0903b1714a514850a89848 100644 --- a/src/metabase/server/middleware/auth.clj +++ b/src/metabase/server/middleware/auth.clj @@ -2,8 +2,10 @@ "Middleware related to enforcing authentication/API keys (when applicable). Unlike most other middleware most of this is not used as part of the normal `app`; it is instead added selectively to appropriate routes." (:require + [clojure.string :as str] [metabase.models.setting :refer [defsetting]] - [metabase.server.middleware.util :as mw.util])) + [metabase.server.middleware.util :as mw.util] + [metabase.util.i18n :refer [deferred-trs]])) (def ^:private ^:const ^String metabase-api-key-header "x-metabase-apikey") @@ -31,6 +33,15 @@ "When set, this API key is required for all API requests." :visibility :internal) +(def mb-api-key-doc-url + "Url for documentation on how to set MB_API_KEY." + "https://www.metabase.com/docs/latest/configuring-metabase/environment-variables#mb_api_key") + +(def key-not-set-response + "Response when the MB_API_KEY is not set." + {:status 403 + :body (deferred-trs "MB_API_KEY is not set. See {0} for details" mb-api-key-doc-url)}) + (defn enforce-api-key "Middleware that enforces validation of the client via API Key, canceling the request processing if the check fails. @@ -43,7 +54,10 @@ This variable only works for /api/notify/db/:id endpoint" [handler] (fn [{:keys [metabase-api-key], :as request} respond raise] - (cond (not metabase-api-key) + (cond (str/blank? (api-key)) + (respond key-not-set-response) + + (not metabase-api-key) (respond mw.util/response-forbidden) (= (api-key) metabase-api-key) diff --git a/test/metabase/api/notify_test.clj b/test/metabase/api/notify_test.clj index 6ec608da6507d48b1ad46d3195e6cf8b883a0875..e51ef120df11375bcf88afad1aca3d74ac36678f 100644 --- a/test/metabase/api/notify_test.clj +++ b/test/metabase/api/notify_test.clj @@ -4,6 +4,7 @@ [clojure.test :refer :all] [metabase.http-client :as client] [metabase.models.database :as database] + [metabase.server.middleware.auth :as mw.auth] [metabase.server.middleware.util :as mw.util] [metabase.sync] [metabase.sync.sync-metadata] @@ -13,11 +14,16 @@ (use-fixtures :once (fixtures/initialize :db :web-server)) -(deftest unauthenticated-test +(deftest authentication-test (testing "POST /api/notify/db/:id" - (testing "endpoint should require authentication" - (is (= (get mw.util/response-forbidden :body) - (client/client :post 403 "notify/db/100")))))) + (testing "endpoint requires MB_API_KEY set" + (mt/with-temporary-setting-values [api-key nil] + (is (= (-> mw.auth/key-not-set-response :body str) + (client/client :post 403 "notify/db/100"))))) + (testing "endpoint requires authentication" + (mt/with-temporary-setting-values [api-key "test-api-key"] ;; set in :test but not in :dev + (is (= (get mw.util/response-forbidden :body) + (client/client :post 403 "notify/db/100"))))))) (def api-headers {:headers {"X-METABASE-APIKEY" "test-api-key" "Content-Type" "application/json"}}) diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj index ba5cc613fb28a32a01f6d3fa6045556d4bf93d69..aae1c069e04a5535e7a6515b8723fb87a23da395 100644 --- a/test/metabase/models/setting_test.clj +++ b/test/metabase/models/setting_test.clj @@ -226,7 +226,7 @@ (tu/do-with-temporary-setting-value setting db-value (fn [] (tu/do-with-temp-env-var-value - (keyword (str "mb-" (name setting))) + (setting/setting-env-map-name (keyword setting)) env-var-value (fn [] (dissoc (#'setting/user-facing-info (#'setting/resolve-setting setting)) @@ -632,7 +632,7 @@ (setting.cache/restore-cache!))))) (fn [thunk] (tu/do-with-temp-env-var-value - (keyword (str "mb-" (name setting-name))) + (setting/setting-env-map-name setting-name) site-wide-value thunk))]] ;; clear out Setting if it was already set for some reason (except for `:only` where this is explicitly diff --git a/test/metabase/public_settings_test.clj b/test/metabase/public_settings_test.clj index 2db1ae84bffcc360895657fd4fbc1ead348ff494..de3e9091bc385be72ee1bb2c66fe068b05fd0903 100644 --- a/test/metabase/public_settings_test.clj +++ b/test/metabase/public_settings_test.clj @@ -62,21 +62,19 @@ (deftest site-url-settings-normalize (testing "We should normalize `site-url` when set via env var we should still normalize it (#9764)" (mt/with-temp-env-var-value [mb-site-url "localhost:3000/"] - (mt/with-temporary-setting-values [site-url nil] - (is (= "localhost:3000/" - (setting/get-value-of-type :string :site-url))) - (is (= "http://localhost:3000" - (public-settings/site-url))))))) + (is (= "localhost:3000/" + (setting/get-value-of-type :string :site-url))) + (is (= "http://localhost:3000" + (public-settings/site-url)))))) (deftest invalid-site-url-env-var-test (testing (str "If `site-url` is set via an env var, and it's invalid, we should return `nil` rather than having the" " whole instance break") (mt/with-temp-env-var-value [mb-site-url "asd_12w31%$;"] - (mt/with-temporary-setting-values [site-url nil] - (is (= "asd_12w31%$;" - (setting/get-value-of-type :string :site-url))) - (is (= nil - (public-settings/site-url))))))) + (is (= "asd_12w31%$;" + (setting/get-value-of-type :string :site-url))) + (is (= nil + (public-settings/site-url)))))) (deftest site-url-should-update-https-redirect-test (testing "Changing `site-url` to non-HTTPS should disable forced HTTPS redirection" diff --git a/test/metabase/server/middleware/auth_test.clj b/test/metabase/server/middleware/auth_test.clj index ff035279e92db14fa517f8ed18e79e91762b6aa1..51595e7be8345e1f43bbc98a08b7abb1a77a9aab 100644 --- a/test/metabase/server/middleware/auth_test.clj +++ b/test/metabase/server/middleware/auth_test.clj @@ -138,11 +138,9 @@ (request-with-api-key "foobar")))))) (testing "no apikey is set, expect 403" - (mt/with-temporary-setting-values [api-key nil] - (is (= mw.util/response-forbidden - (api-key-enforced-handler - (ring.mock/request :get "/anyurl"))))) - (mt/with-temporary-setting-values [api-key ""] - (is (= mw.util/response-forbidden - (api-key-enforced-handler - (ring.mock/request :get "/anyurl"))))))) + (doseq [api-key-value [nil ""]] + (testing (str "when key is " ({nil "nil" "" "empty"} api-key-value)) + (mt/with-temporary-setting-values [api-key api-key-value] + (is (= mw.auth/key-not-set-response + (api-key-enforced-handler + (ring.mock/request :get "/anyurl"))))))))) diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index 9e8bf777a4a61bef8f802f9d981456419c7fe697..3a8d491c8c29d1420883a13f4f2f9ac10e28bfce 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -424,8 +424,8 @@ (catch Exception e (when-not raw-setting? (throw e))))] - (if-let [env-var-value (and (not raw-setting?) (#'setting/env-var-value setting-k))] - (do-with-temp-env-var-value setting env-var-value thunk) + (if (and (not raw-setting?) (#'setting/env-var-value setting-k)) + (do-with-temp-env-var-value (setting/setting-env-map-name setting-k) value thunk) (let [original-value (if raw-setting? (db/select-one-field :value Setting :key setting-k) (#'setting/get setting-k))]