Functions used to calculate the permissions needed to run a query based on old-style DATA ACCESS PERMISSIONS. The only thing that is subject to these sorts of checks are ad-hoc queries, i.e. queries that have not yet been saved as a Card. Saved Cards are subject to the permissions of the Collection to which they belong. | (ns metabase.models.query.permissions (:require [clojure.set :as set] [clojure.walk :as walk] [metabase.api.common :as api] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.lib.core :as lib] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.util.match :as lib.util.match] [metabase.models.data-permissions :as data-perms] [metabase.models.interface :as mi] [metabase.models.permissions :as perms] [metabase.permissions.util :as perms.u] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.store :as qp.store] [metabase.query-processor.util :as qp.util] [metabase.request.core :as request] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) |
(set! *warn-on-reflection* true) | |
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 api/*current-user-id*) :permissions-error? true} additional-ex-data)))) |
---------------------------------------------- Permissions Checking ---------------------------------------------- | |
Is calculating permissions for queries complicated? Some would say so. Refer to this handy flow chart to see how things get calculated.
native-query-perms legacy-mbql-required-perms | no source card <--------+------> has source card ↓ ↓ {:perms/view-data {table-id :unrestricted}} source-card-read-perms | |
Merge function which takes the union of two sets of IDs, if they are both sets | (defn- merge-source-ids [val1 val2] (cond ;; Merge sets of table or card IDs (and (set? val1) (set? val2)) (set/union val1 val2) ;; Booleans should only ever be `:native? true`, but make sure we propogate truthy values (and (boolean? val1) (boolean? val2)) (or val1 val2) ;; Safeguard; should not be hit :else (throw (ex-info "Don't know how to merge values!" {:val1 val1 :val2 val2})))) |
(mu/defn query->source-ids :- [:maybe [:map [:table-ids {:optional true} [:set ::lib.schema.id/table]] [:card-ids {:optional true} [:set ::lib.schema.id/card]] [:native? {:optional true} :boolean]]] "Return a map containing table IDs and/or card IDs referenced by `query`, and/or the :native? boolean flag indicating a native query or subquery. Intended to be used in the context of permissions enforcement." [query :- :map] (apply merge-with merge-source-ids (lib.util.match/match query ;; If we come across a native query, replace it with a card ID if it came from a source card, so we can check ;; permissions on the card and not necessarily require full native query access to the DB (m :guard (every-pred map? :native)) (if-let [source-card-id (:qp/stage-is-from-source-card m)] {:card-ids #{source-card-id}} {:native? true}) (m :guard (every-pred map? #(pos-int? (:source-table %)))) (merge-with merge-source-ids {:table-ids #{(:source-table m)}} ;; If there's a source card associated with a table ID, include it so that we can ensure that ;; ad-hoc queries don't access cards with no collection perms (when-let [source-card-id (:qp/stage-is-from-source-card m)] {:card-ids #{source-card-id}}) (query->source-ids (dissoc m :source-table)))))) | |
Returns a sequence of all :source-table IDs referenced by a query. Convenience wrapper around | (mu/defn query->source-table-ids [query :- :map] (when (seq query) (:table-ids (query->source-ids query)))) |
A map from card IDs to card instances with the collection_id (possibly nil). Useful when bulk loading cards from different databases. | (def ^:dynamic *card-instances* nil) |
(mu/defn- card-instance :- [:and (ms/InstanceOf :model/Card) [:map [:collection_id [:maybe ms/PositiveInt]]]] [card-id :- ::lib.schema.id/card] (or (get *card-instances* card-id) (if (qp.store/initialized?) (when-let [{:keys [collection-id]} (lib.metadata/card (qp.store/metadata-provider) card-id)] (t2/instance :model/Card {:collection_id collection-id})) (t2/select-one [:model/Card :collection_id] :id card-id)) (throw (Exception. (tru "Card {0} does not exist." card-id))))) | |
(mu/defn- source-card-read-perms :- [:set perms.u/PathSchema] "Calculate the permissions needed to run an ad-hoc query that uses a Card with `source-card-id` as its source query." [source-card-id :- ::lib.schema.id/card] (mi/perms-objects-set (card-instance source-card-id) :read)) | |
(defn- preprocess-query [query] ;; ignore the current user for the purposes of calculating the permissions required to run the query. Don't want the ;; preprocessing to fail because current user doesn't have permissions to run it when we're not trying to run it at ;; all (request/as-admin ((requiring-resolve 'metabase.query-processor.preprocess/preprocess) query))) | |
Return the union of all the | (defn- referenced-card-ids [query] (let [all-ids (atom #{})] (walk/postwalk (fn [form] (when (map? form) (when-let [ids (not-empty (::referenced-card-ids form))] (swap! all-ids set/union ids))) form) query) (not-empty @all-ids))) |
(defn- native-query-perms [query] (merge {:perms/create-queries :query-builder-and-native :perms/view-data :unrestricted} (when-let [card-ids (referenced-card-ids query)] {:paths (into #{} (mapcat (fn [card-id] (mi/perms-objects-set (card-instance card-id) :read))) card-ids)}))) | |
(defn- legacy-mbql-required-perms [query {:keys [throw-exceptions? already-preprocessed?]}] (try (let [query (mbql.normalize/normalize query)] ;; if we are using a Card as our source, our perms are that Card's (i.e. that Card's Collection's) read perms (if-let [source-card-id (qp.util/query->source-card-id query)] {:paths (source-card-read-perms source-card-id)} ;; otherwise if there's no source card then calculate perms based on the Tables referenced in the query (let [query (cond-> query (not already-preprocessed?) preprocess-query) {:keys [table-ids card-ids native?]} (query->source-ids query)] (merge (when (seq card-ids) {:card-ids card-ids}) (when (seq table-ids) {:perms/create-queries (zipmap table-ids (repeat :query-builder)) :perms/view-data (zipmap table-ids (repeat :unrestricted))}) (when native? (native-query-perms query)))))) ;; if for some reason we can't expand the Card (i.e. it's an invalid legacy card) just return a set of permissions ;; that means no one will ever get to see it (catch Throwable e (let [e (ex-info "Error calculating permissions for query" {:query (or (u/ignore-exceptions (mbql.normalize/normalize query)) query)} e)] (if throw-exceptions? (throw e) (log/error e))) {:perms/create-queries {0 :query-builder}}))) ; table 0 will never exist | |
For pMBQL queries: for now, just convert it to legacy by running it thru the QP preprocessor, then hand off to the legacy implementation(s) of [[required-perms]]. | (defn- pmbql-required-perms [query perms-opts] (let [query (lib/normalize query) ;; convert it to legacy by running it thru the QP preprocessor. legacy-query (preprocess-query query)] (assert (#{:query :native} (:type legacy-query)) (format "Expected QP preprocessing to return legacy MBQL query, got: %s" (pr-str legacy-query))) (legacy-mbql-required-perms legacy-query perms-opts))) |
Returns a map representing the permissions requried to run | (defn required-perms-for-query [query & {:as perms-opts}] (if (empty? query) {} (let [query-type (lib/normalized-query-type query)] (case query-type :native (native-query-perms query) :query (legacy-mbql-required-perms query perms-opts) :mbql/query (pmbql-required-perms query perms-opts) (throw (ex-info (tru "Invalid query type: {0}" query-type) {:query query})))))) |
Checks that the current user has at least | (defn- has-perm-for-db? [perm-type required-perm gtap-perms db-id] (or (data-perms/at-least-as-permissive? perm-type (data-perms/full-db-permission-for-user api/*current-user-id* perm-type db-id) required-perm) (when gtap-perms (data-perms/at-least-as-permissive? perm-type gtap-perms required-perm)))) |
Checks that the current user has the permissions for tables specified in | (defn- has-perm-for-table? [perm-type table-id->required-perm gtap-table-perms db-id] (let [table-id->has-perm? (into {} (for [[table-id required-perm] table-id->required-perm] [table-id (boolean (or (data-perms/user-has-permission-for-table? api/*current-user-id* perm-type required-perm db-id table-id) (when-let [gtap-perm (if (keyword? gtap-table-perms) ;; gtap-table-perms can be a keyword representing the DB permission... gtap-table-perms ;; ...or a map from table IDs to table permissions (get gtap-table-perms table-id))] (data-perms/at-least-as-permissive? perm-type gtap-perm required-perm))))]))] (every? true? (vals table-id->has-perm?)))) |
(mu/defn has-perm-for-query? :- :boolean "Returns true when the query is accessible for the given perm-type and required-perms for individual tables, or the entire DB, false otherwise. Only throws if the permission format is incorrect." [{{gtap-perms :gtaps} ::perms, db-id :database :as _query} perm-type required-perms] (boolean (if-let [db-or-table-perms (perm-type required-perms)] ;; In practice, `view-data` will be defined at the table-level, and `create-queries` will either be table-level ;; or :query-builder-and-native for the entire DB. But we should enforce whatever `required-perms` are provided, ;; in case that ever changes. (cond (keyword? db-or-table-perms) (has-perm-for-db? perm-type db-or-table-perms (perm-type gtap-perms) db-id) (map? db-or-table-perms) (has-perm-for-table? perm-type db-or-table-perms (perm-type gtap-perms) db-id) :else (throw (ex-info (tru "Invalid permissions format") required-perms))) true))) | |
Check that the current user has permissions to read Card with | (mu/defn check-card-read-perms [database-id :- ::lib.schema.id/database card-id :- ::lib.schema.id/card] (qp.store/with-metadata-provider database-id (let [card (or (some-> (lib.metadata.protocols/card (qp.store/metadata-provider) card-id) (update-keys u/->snake_case_en) (vary-meta assoc :type :model/Card)) (throw (ex-info (tru "Card {0} does not exist." card-id) {:type qp.error-type/invalid-query :card-id card-id})))] (log/tracef "Required perms to run Card: %s" (pr-str (mi/perms-objects-set card :read))) (when-not (mi/can-read? card) (throw (perms-exception (tru "You do not have permissions to view Card {0}." (pr-str card-id)) (mi/perms-objects-set card :read) {:card-id card-id})))))) |
Checks whether the current user has sufficient view data and query permissions to run If the [:gtap ::perms] path is present in the query, these perms are implicitly granted to the current user. | (defn check-data-perms [{{gtap-perms :gtaps} ::perms, :as query} required-perms & {:keys [throw-exceptions?] :or {throw-exceptions? true}}] (try ;; Check any required v1 paths (when-let [paths (:paths required-perms)] (let [paths-excluding-gtap-paths (set/difference paths (:paths gtap-perms))] (or (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* paths-excluding-gtap-paths) (throw (perms-exception paths))))) ;; Check view-data and create-queries permissions, for individual tables or the entire DB: (when (or (not (has-perm-for-query? query :perms/view-data required-perms)) (not (has-perm-for-query? query :perms/create-queries required-perms))) (throw (perms-exception required-perms))) true (catch clojure.lang.ExceptionInfo e (if throw-exceptions? (throw e) false)))) |
Return | (mu/defn can-run-query? [{database-id :database :as query}] (try (let [required-perms (required-perms-for-query query)] (check-data-perms query required-perms) ;; Check card read permissions for any cards referenced in subqueries! (doseq [card-id (:card-ids required-perms)] (check-card-read-perms database-id card-id)) true) (catch clojure.lang.ExceptionInfo _e false))) |
Does the current user have permissions to run an ad-hoc query against the Table with | (defn can-query-table? [database-id table-id] (can-run-query? {:database database-id :type :query :query {:source-table table-id}})) |