Skip to content
Snippets Groups Projects
Unverified Commit 5e49508d authored by dpsutton's avatar dpsutton Committed by GitHub
Browse files

Handle email password obfuscation (#23925)

The frontend receives obfuscated values for settings marked sensitive:

```clojure
(defsetting email-smtp-password
  (deferred-tru "SMTP password.")
  :sensitive? true)
```

and over the wire:
```javascript
    {
        "key": "email-smtp-password",
        "value": "**********ig",
        "is_env_setting": false,
        "env_name": "MB_EMAIL_SMTP_PASSWORD",
        "description": "SMTP password.",
        "default": null
    },
```

So the frontend form never has a valid password in it. When you edit
email settings, we check if those are valid, and if so, commit the
settings. But with the wrong password email settings are almost always
wrong. You have to re-type the email password just to change other
settings, like the friendly name, reply-to email address, etc.

So we recognize that we've received the obfuscated value, swap in the
real value for the email connection test, and then obfuscate the
password in the response. If we do not receive an obfuscated value, just
leave it alone and return the value anyways (they typed it in, so seems
safe to send it back).

Note that there is a function in models.setting called
`obfuscated-value?` that checks strings against a regex of
`#"^\*{10}.{2}$"`. This is used to never set settings to an obfuscated
value. But its a bit less sensitive than our purposes need. We have a
real value and an obfuscated value so we can check if the obfuscated
value is based on the real value. (obfuscate-value "password") ->
"**********rd" . Whereas we might recognize "**********AA" as the
obfuscated value if we reused that helper function.

NOTE this could be weird if anyone's password changes from "password" to
"**********rd" but the chances of that happening are miniscule.

Had thought to have the FE not send a value at all if it is unchanged
but this had some far-reaching implications that we don't want to tackle
so close to the release. There's an associated issue for LDAP settings
that is largely the same as this:
https://github.com/metabase/metabase/issues/16708 But it is not clear to
me if these are the only two places or if there could be others. Until
that research is done we can just accept this patch as is and come up
with a systematic approach in the future.

But it does seem only three settings are sensitive:
```clojure
setting=> (->> (vals @registered-settings)
               (filter :sensitive?)
               (map :name))
(:saml-keystore-password :email-smtp-password :ldap-password)
```

The direct setting apis won't write setting values which appear as
obfuscated, but we have other endpoints that take collections of
settings as a unit (for instance, the endpoint /api/email that takes all
of the email settings here to test if they work together).
parent 21aa0053
No related branches found
No related tags found
No related merge requests found
......@@ -73,20 +73,27 @@
[:as {settings :body}]
{settings su/Map}
(validation/check-has-application-permission :setting)
(let [settings (-> settings
(select-keys (keys mb-to-smtp-settings))
(set/rename-keys mb-to-smtp-settings))
settings (cond-> settings
(string? (:port settings)) (update :port #(Long/parseLong ^String %))
(string? (:security settings)) (update :security keyword))
;; the frontend has access to an obfuscated version of the password. Watch for whether it sent us a new password or
;; the obfuscated version
(let [obfuscated? (and (:email-smtp-password settings) (email/email-smtp-password)
(= (:email-smtp-password settings) (setting/obfuscate-value (email/email-smtp-password))))
settings (-> (cond-> settings
obfuscated?
(assoc :email-smtp-password (email/email-smtp-password)))
(select-keys (keys mb-to-smtp-settings))
(set/rename-keys mb-to-smtp-settings))
settings (cond-> settings
(string? (:port settings)) (update :port #(Long/parseLong ^String %))
(string? (:security settings)) (update :security keyword))
response (email/test-smtp-connection settings)]
(if-not (::email/error response)
;; test was good, save our settings
(assoc (setting/set-many! (set/rename-keys response (set/map-invert mb-to-smtp-settings)))
:with-corrections (let [[_ corrections] (data/diff settings response)]
(-> corrections
(set/rename-keys (set/map-invert mb-to-smtp-settings))
humanize-email-corrections)))
(cond-> (assoc (setting/set-many! (set/rename-keys response (set/map-invert mb-to-smtp-settings)))
:with-corrections (let [[_ corrections] (data/diff settings response)]
(-> corrections
(set/rename-keys (set/map-invert mb-to-smtp-settings))
humanize-email-corrections)))
obfuscated? (update :email-smtp-password setting/obfuscate-value))
;; test failed, return response message
{:status 400
:body (humanize-error-messages response)})))
......
(ns metabase.api.email-test
(:require [clojure.test :refer :all]
(:require [clojure.string :as str]
[clojure.test :refer :all]
[metabase.api.email :as api.email]
[metabase.email :as email]
[metabase.models.setting :as setting]
......@@ -90,7 +91,33 @@
(is (= (if success?
default-email-settings
original-values)
(email-settings)))))))))))
(email-settings))))))))))
(testing "Updating values with obfuscated password (#23919)"
(mt/with-temporary-setting-values [email-from-address "notifications@metabase.com"
email-from-name "Sender Name"
email-reply-to ["reply-to@metabase.com"]
email-smtp-host "www.test.com"
email-smtp-password "preexisting"]
(with-redefs [email/test-smtp-connection (fn [settings]
(let [obfuscated? (str/starts-with? (:pass settings) "****")]
(is (not obfuscated?) "We received an obfuscated password!")
(if obfuscated?
{::email/error (ex-info "Sent obfuscated password" {})}
settings)))]
(testing "If we don't change the password we don't see the password"
(let [payload (-> (email-settings)
;; user changes one property
(assoc :email-from-name "notifications")
;; the FE will have an obfuscated value
(update :email-smtp-password setting/obfuscate-value))
response (mt/user-http-request :crowberto :put 200 "email" payload)]
(is (= (setting/obfuscate-value "preexisting") (:email-smtp-password response)))))
(testing "If we change the password we can receive the password"
(let [payload (-> (email-settings)
;; user types in a new password
(assoc :email-smtp-password "new-password"))
response (mt/user-http-request :crowberto :put 200 "email" payload)]
(is (= "new-password" (:email-smtp-password response)))))))))
(deftest clear-email-settings-test
(testing "DELETE /api/email"
......
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