Ring middleware related to session and API-key based authentication (binding current user and permissions). How do authenticated API requests work? There are two main paths to authentication: a session or an API key. For session authentication, Metabase first looks for a cookie called Finally we'll check for the presence of a The second main path to authentication is an API key. For this, we look at the | (ns metabase.server.middleware.session (:require [honey.sql.helpers :as sql.helpers] [java-time.api :as t] [metabase.api.common :as api] [metabase.config :as config] [metabase.core.initialization-status :as init-status] [metabase.db :as mdb] [metabase.driver.sql.query-processor :as sql.qp] [metabase.models.api-key :as api-key] [metabase.models.user :as user] [metabase.premium-features.core :as premium-features] [metabase.request.core :as request] [metabase.util.i18n :as i18n] [metabase.util.log :as log] [metabase.util.password :as u.password] [toucan2.core :as t2] [toucan2.pipeline :as t2.pipeline])) |
(set! *warn-on-reflection* true) | |
+----------------------------------------------------------------------------------------------------------------+ | wrap-session-id | +----------------------------------------------------------------------------------------------------------------+ | |
(def ^:private ^String metabase-session-header "x-metabase-session") | |
Attempt to add | (defmulti ^:private wrap-session-id-with-strategy
{:arglists '([strategy request])}
(fn [strategy _]
strategy)) |
(defmethod wrap-session-id-with-strategy :embedded-cookie
[_ {:keys [cookies headers], :as request}]
(when-let [session (get-in cookies [request/metabase-embedded-session-cookie :value])]
(when-let [anti-csrf-token (get headers request/anti-csrf-token-header)]
(assoc request :metabase-session-id session, :anti-csrf-token anti-csrf-token :metabase-session-type :full-app-embed)))) | |
(defmethod wrap-session-id-with-strategy :normal-cookie
[_ {:keys [cookies], :as request}]
(when-let [session (get-in cookies [request/metabase-session-cookie :value])]
(when (seq session)
(assoc request :metabase-session-id session :metabase-session-type :normal)))) | |
(defmethod wrap-session-id-with-strategy :header
[_ {:keys [headers], :as request}]
(when-let [session (get headers metabase-session-header)]
(when (seq session)
(assoc request :metabase-session-id session)))) | |
(defmethod wrap-session-id-with-strategy :best
[_ request]
(some
(fn [strategy]
(wrap-session-id-with-strategy strategy request))
[:embedded-cookie :normal-cookie :header])) | |
Middleware that sets the | (defn wrap-session-id
[handler]
(fn [request respond raise]
(let [request (or (wrap-session-id-with-strategy :best request)
request)]
(handler request respond raise)))) |
+----------------------------------------------------------------------------------------------------------------+ | wrap-current-user-info | +----------------------------------------------------------------------------------------------------------------+ | |
Because this query runs on every single API request it's worth it to optimize it a bit and only compile it to SQL once rather than every time | (def ^:private ^{:arglists '([db-type max-age-minutes session-type enable-advanced-permissions?])} session-with-id-query
(memoize
(fn [db-type max-age-minutes session-type enable-advanced-permissions?]
(first
(t2.pipeline/compile*
(cond-> {:select [[:session.user_id :metabase-user-id]
[:user.is_superuser :is-superuser?]
[:user.locale :user-locale]]
:from [[:core_session :session]]
:left-join [[:core_user :user] [:= :session.user_id :user.id]]
:where [:and
[:= :user.is_active true]
[:= :session.id [:raw "?"]]
(let [oldest-allowed [:inline (sql.qp/add-interval-honeysql-form db-type
:%now
(- max-age-minutes)
:minute)]]
[:> :session.created_at oldest-allowed])
[:= :session.anti_csrf_token (case session-type
:normal nil
:full-app-embed [:raw "?"])]]
:limit [:inline 1]}
enable-advanced-permissions?
(->
(sql.helpers/select
[:pgm.is_group_manager :is-group-manager?])
(sql.helpers/left-join
[:permissions_group_membership :pgm] [:and
[:= :pgm.user_id :user.id]
[:is :pgm.is_group_manager true]])))))))) |
See above: because this query runs on every single API request (with an API Key) it's worth it to optimize it a bit and only compile it to SQL once rather than every time | (def ^:private ^{:arglists '([enable-advanced-permissions?])} user-data-for-api-key-prefix-query
(memoize
(fn [enable-advanced-permissions?]
(first
(t2.pipeline/compile*
(cond-> {:select [[:api_key.user_id :metabase-user-id]
[:api_key.key :api-key]
[:user.is_superuser :is-superuser?]
[:user.locale :user-locale]]
:from :api_key
:left-join [[:core_user :user] [:= :api_key.user_id :user.id]]
:where [:and
[:= :user.is_active true]
[:= :api_key.key_prefix [:raw "?"]]]
:limit [:inline 1]}
enable-advanced-permissions?
(->
(sql.helpers/select
[:pgm.is_group_manager :is-group-manager?])
(sql.helpers/left-join
[:permissions_group_membership :pgm] [:and
[:= :pgm.user_id :user.id]
[:is :pgm.is_group_manager true]])))))))) |
Return User ID and superuser status for Session with | (defn- current-user-info-for-session
[session-id anti-csrf-token]
(when (and session-id (init-status/complete?))
(let [sql (session-with-id-query (mdb/db-type)
(config/config-int :max-session-age)
(if (seq anti-csrf-token) :full-app-embed :normal)
(premium-features/enable-advanced-permissions?))
params (concat [session-id]
(when (seq anti-csrf-token)
[anti-csrf-token]))]
(some-> (t2/query-one (cons sql params))
;; is-group-manager? could return `nil, convert it to boolean so it's guaranteed to be only true/false
(update :is-group-manager? boolean))))) |
(def ^:private api-key-that-should-never-match (str (random-uuid))) (def ^:private hash-that-should-never-match (u.password/hash-bcrypt "password")) | |
Password check that will always fail, used to avoid exposing any info about existing users or API keys via timing attacks. | (defn do-useless-hash [] (u.password/verify-password api-key-that-should-never-match "" hash-that-should-never-match)) |
(defn- matching-api-key? [{:keys [api-key] :as _user-data} passed-api-key]
;; if we get an API key, check the hash against the passed value. If not, don't reveal info via a timing attack - do
;; a useless hash, *then* return `false`.
(if api-key
(u.password/verify-password passed-api-key api-key)
(do-useless-hash))) | |
Return User ID and superuser status for an API Key with `api-key-id | (defn- current-user-info-for-api-key
[api-key]
(when (and api-key (init-status/complete?))
(let [user-data (some-> (t2/query-one (cons (user-data-for-api-key-prefix-query
(premium-features/enable-advanced-permissions?))
[(api-key/prefix api-key)]))
(update :is-group-manager? boolean))]
(when (matching-api-key? user-data api-key)
(dissoc user-data :api-key))))) |
(defn- merge-current-user-info
[{:keys [metabase-session-id anti-csrf-token], {:strs [x-metabase-locale x-api-key]} :headers, :as request}]
(merge
request
(or (current-user-info-for-session metabase-session-id anti-csrf-token)
(current-user-info-for-api-key x-api-key))
(when x-metabase-locale
(log/tracef "Found X-Metabase-Locale header: using %s as user locale" (pr-str x-metabase-locale))
{:user-locale (i18n/normalized-locale-string x-metabase-locale)}))) | |
Add | (defn wrap-current-user-info
[handler]
(fn [request respond raise]
(handler (merge-current-user-info request) respond raise))) |
+----------------------------------------------------------------------------------------------------------------+ | bind-current-user | +----------------------------------------------------------------------------------------------------------------+ | |
If [[metabase.api.common/current-user-permissions-set]] is bound, reset it so it gets recalculated on next use. Called by [[metabase.permissions.models.permissions/delete-related-permissions!]] and [[metabase.permissions.models.permissions/grant-permissions!]], mostly as a convenience for tests that bind a current user and then grant or revoke permissions for that user without rebinding it. this is actually used by [[metabase.permissions.models.permissions/clear-current-user-cached-permissions!]] TODO -- then why doesn't it live there??? Not one single thing this touches is part of this namespace. | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn clear-current-user-cached-permissions-set!
[]
(when-let [current-user-id api/*current-user-id*]
;; [[api/*current-user-permissions-set*]] is dynamically bound
(when (get (get-thread-bindings) #'api/*current-user-permissions-set*)
(.set #'api/*current-user-permissions-set* (delay (user/permissions-set current-user-id)))))
nil) |
(defmacro ^:private with-current-user-for-request [request & body] `(request/do-with-current-user ~request (fn [] ~@body))) | |
Middleware that binds [[metabase.api.common/current-user]], [[current-user-id]], [[is-superuser?]], [[current-user-permissions-set]], and [[metabase.models.setting/user-local-values]].
| (defn bind-current-user
[handler]
(fn [request respond raise]
(with-current-user-for-request request
(handler request respond raise)))) |
+----------------------------------------------------------------------------------------------------------------+ | reset-cookie-timeout | +----------------------------------------------------------------------------------------------------------------+ | |
Implementation for | (defn reset-session-timeout*
[request response request-time]
(if (and
;; Only reset the timeout if the request includes a session cookie.
(:metabase-session-type request)
;; Do not reset the timeout if it is being updated in the response, e.g. if it is being deleted
(not (contains? (:cookies response) request/metabase-session-timeout-cookie)))
(request/set-session-timeout-cookie response request (:metabase-session-type request) request-time)
response)) |
Middleware that resets the expiry date on session cookies according to the session-timeout setting. Will not change anything if the session-timeout setting is nil, or the timeout cookie has already expired. | (defn reset-session-timeout
[handler]
(fn [request respond raise]
(let [;; The expiry time for the cookie is relative to the time the request is received, rather than the time of the
;; response.
request-time (t/zoned-date-time (t/zone-id "GMT"))]
(handler request
(fn [response]
(respond (reset-session-timeout* request response request-time)))
raise)))) |