Middleware for checking that the current user has permissions to run the current query. | (ns metabase.query-processor.middleware.permissions (:require [clojure.set :as set] [metabase.api.common :refer [*current-user-id* *current-user-permissions-set*]] [metabase.audit :as audit] [metabase.lib.core :as lib] [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.walk :as lib.walk] [metabase.models.data-permissions :as data-perms] [metabase.models.query.permissions :as query-perms] [metabase.public-settings.premium-features :refer [defenterprise]] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.store :as qp.store] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu])) |
ID of the Card currently being executed, if there is one. Bind this in a Card-execution so we will use Card [Collection] perms checking rather than ad-hoc perms checking. | (def ^:dynamic *card-id* nil) |
Returns an ExceptionInfo instance containing data relevant for a permissions error. | (defn perms-exception ([required-perms] (perms-exception (tru "You do not have permissions to run this query.") required-perms)) ([message required-perms & [additional-ex-data]] (ex-info message (merge {:type qp.error-type/missing-required-permissions :required-permissions required-perms :actual-permissions (data-perms/permissions-for-user *current-user-id*) :permissions-error? true} additional-ex-data)))) |
OSS implementation always returns | (defenterprise check-block-permissions metabase-enterprise.advanced-permissions.models.permissions.block-permissions [_query]) |
(defn- throw-inactive-table-error [{db-id :id db-name :name} {table-id :id table-name :name schema :schema}] ;; We don't cache perms for inactive tables, so we need to manually bypass the cache here (binding [data-perms/*use-perms-cache?* false] (let [show-table-name? (data-perms/user-has-permission-for-table? *current-user-id* :perms/view-data :unrestricted db-id table-id)] (throw (Exception. (tru "Table {0} is inactive." (if show-table-name? (format "\"%s.%s.%s\ db-name schema table-name) table-id))))))) | |
Throws an exception if any of the tables referenced by this query are marked as inactive in the app DB. These queries would (likely) fail anyway since an inactive table one is either deleted, or Metabase's connection doesn't have access to it. But we can reject them preemptively for a more consistent experience, and to avoid needing to cache permissions for inactive tables. | (defn- check-query-does-not-access-inactive-tables [{database-id :database, :as outer-query}] (qp.store/with-metadata-provider database-id (let [table-ids (query-perms/query->source-table-ids outer-query)] (doseq [table-id table-ids] (let [table (lib.metadata.protocols/table (qp.store/metadata-provider) table-id)] (when-not (:active table) (throw-inactive-table-error (lib.metadata.protocols/database (qp.store/metadata-provider)) table))))))) |
Used to allow users looking at a dashboard to view (possibly chained) filters. | (def ^:dynamic *param-values-query* false) |
OSS implementation always throws an exception since queries over the audit DB are not permitted. | (defenterprise check-audit-db-permissions metabase-enterprise.audit-app.permissions [query] (throw (ex-info (tru "Querying this database requires the audit-app feature flag") query))) |
Pre-processing middleware. Removes the | (defn remove-permissions-key [query] (dissoc query ::query-perms/perms)) |
Pre-processing middleware. Removes any instances of the | (defn remove-source-card-keys [query] (lib.walk/walk query (fn [_query _path-type _path stage-or-join] (dissoc stage-or-join :qp/stage-is-from-source-card)))) |
Check that User with | (mu/defn check-query-permissions* [{database-id :database, {gtap-perms :gtaps} ::perms :as outer-query} :- [:map [:database ::lib.schema.id/database]]] (when *current-user-id* (log/tracef "Checking query permissions. Current user permissions = %s" (pr-str (data-perms/permissions-for-user *current-user-id*))) (when (= audit/audit-db-id database-id) (check-audit-db-permissions outer-query)) (check-query-does-not-access-inactive-tables outer-query) (let [card-id (or *card-id* (:qp/source-card-id outer-query)) required-perms (query-perms/required-perms-for-query outer-query :already-preprocessed? true) source-card-ids (set/difference (:card-ids required-perms) (:card-ids gtap-perms))] ;; On EE, check block permissions up front for all queries. If block perms are in place, reject all native queries ;; (unless overriden by `gtap-perms`) and any queries that touch blocked tables/DBs (check-block-permissions outer-query) (cond card-id (query-perms/check-card-read-perms database-id card-id) ;; set when querying for field values of dashboard filters, which only require ;; collection perms for the dashboard and not ad-hoc query perms *param-values-query* (when-not (query-perms/has-perm-for-query? outer-query :perms/view-data required-perms) (throw (query-perms/perms-exception required-perms))) ;; Ad-hoc query (not a saved question) :else (do (query-perms/check-data-perms outer-query required-perms :throw-exceptions? true) ;; Recursively check permissions for any source Cards (doseq [card-id source-card-ids] (let [{query :dataset-query} (lib.metadata.protocols/card (qp.store/metadata-provider) card-id)] (binding [*card-id* card-id] (check-query-permissions* query)))) ;; Recursively check permissions for any Cards referenced by this query via template tags (doseq [{query :dataset-query} (lib/template-tags-referenced-cards (lib/query (qp.store/metadata-provider) outer-query))] (check-query-permissions* query))))))) |
Middleware that check that the current user has permissions to run the current query. This only applies if
| (defn check-query-permissions [qp] (fn [query rff] (check-query-permissions* query) (qp query rff))) |
+----------------------------------------------------------------------------------------------------------------+ | Writeback fns | +----------------------------------------------------------------------------------------------------------------+ | |
Check that User with | (mu/defn check-query-action-permissions* [{database-id :database, :as outer-query} :- [:map [:database ::lib.schema.id/database] [:type [:enum :query :native]]]] (log/tracef "Checking query permissions. Current user perms set = %s" (pr-str @*current-user-permissions-set*)) (when *card-id* (query-perms/check-card-read-perms database-id *card-id*)) (when-not (query-perms/check-data-perms outer-query (query-perms/required-perms-for-query outer-query :already-preprocessed? true) :throw-exceptions? false) (check-block-permissions outer-query))) |
Middleware that check that the current user has permissions to run the current query action. | (defn check-query-action-permissions [qp] (fn [query rff] (check-query-action-permissions* query) (qp query rff))) |
+----------------------------------------------------------------------------------------------------------------+ | Non-middleware util fns | +----------------------------------------------------------------------------------------------------------------+ | |
If current user is bound, do they have ad-hoc native query permissions for | (defn current-user-has-adhoc-native-query-perms? [{database-id :database, :as _query}] (or (not *current-user-id*) (= (data-perms/full-db-permission-for-user *current-user-id* :perms/create-queries database-id) :query-builder-and-native))) |
Check that the current user (if bound) has adhoc native query permissions to run | (defn check-current-user-has-adhoc-native-query-perms [{database-id :database, :as query}] (when-not (current-user-has-adhoc-native-query-perms? query) (throw (perms-exception {database-id {:perms/create-queries :query-builder-and-native}})))) |