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. TODO -- does this belong HERE or in the | (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.interface :as mi] [metabase.permissions.core :as perms] [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 (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/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
(perms/at-least-as-permissive? perm-type
(perms/full-db-permission-for-user api/*current-user-id* perm-type db-id)
required-perm)
(when gtap-perms
(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 (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))]
(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}})) |