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 nil because block permissions are an EE-only feature.

(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 ::perms key from the query. This is where we store important permissions information like perms coming from sandboxing (GTAPs). This is programatically added by middleware when appropriate, but we definitely don't want users passing it in themselves. So remove it if it's present.

(defn remove-permissions-key
  [query]
  (dissoc query ::query-perms/perms))

Pre-processing middleware. Removes any instances of the :qp/stage-is-from-source-card key which is added by the fetch-source-query middleware when source cards are resolved in a query. Since we rely on this for permission enforcement, we want to disallow users from passing it in themselves (like remove-permissions-key above).

(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 user-id has permissions to run query, or throw an exception.

(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 *current-user-id* is bound. In other cases, like when running public Cards or sending pulses, permissions need to be checked separately before allowing the relevant objects to be create (e.g., when saving a new Pulse or 'publishing' a Card).

(defn check-query-permissions
  [qp]
  (fn [query rff]
    (check-query-permissions* query)
    (qp query rff)))

+----------------------------------------------------------------------------------------------------------------+ | Writeback fns | +----------------------------------------------------------------------------------------------------------------+

Check that User with user-id has permissions to run query action query, or throw an exception.

(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 query's database? (This is used by [[metabase.query-processor.compile/compile]] and the [[metabase.query-processor.middleware.catch-exceptions/catch-exceptions]] middleware to check the user should be allowed to see the native query before converting the MBQL query to native.)

(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 query, or throw an Exception. (This is used by the POST /api/dataset/native endpoint to check perms before converting an MBQL query to native.)

(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}}))))