Low-level Metabase permissions system definition and utility functions. The Metabase permissions system is based around permissions paths that are granted to individual [[metabase.models.permissions-group]]s. Core conceptsPermissions are granted to individual [[metabase.models.permissions-group]]s, and Users are members of one or more
Permissions Groups. Permissions Groups are like 'roles' in other permissions systems. There are a few 'magic'
Permissions Groups: the [[metabase.models.permissions-group/all-users]] Group, of which every User is a member and
cannot be removed; and the [[metabase.models.permissions-group/admin]] Group, of which every superuser (i.e., every
User with The permissions needed to perform an action are represented as slash-delimited path strings, for example
Permissions paths use a prefix system where a User is normally allowed to perform any action if one of their Groups
has any permissions entry that is a prefix for the permission required to perform that action. For example, if
reading Database 1 requires the permission This prefix system allows us to easily and efficiently query the application database to find relevant matching
permissions matching an path or path using The union of all permissions the current User's gets from all groups of which they are a member are automatically bound to [[metabase.api.common/current-user-permissions-set]] by [[metabase.server.middleware.session/bind-current-user]] for every REST API request, and in other places when queries are ran in a non-API thread (e.g. for scheduled Dashboard Subscriptions). Different types of permissionsThere are two main types of permissions:
Enterprise-only permissions and "anti-permissions"In addition to data permissions and Collection permissions, a User can also be granted four additional types of permissions.
Determining CRUD permissions in the REST APIREST API permissions checks are generally done in various The implementation of these methods is up to individual models. The majority of implementations check whether [[metabase.api.common/current-user-permissions-set]] includes permissions for a given path (action) using [[set-has-full-permissions?]], or for a set of paths using [[set-has-full-permissions-for-set?]]. Other implementations check whether a user has partial permissions for a path or set
using [[set-has-partial-permissions?]] or [[set-has-partial-permissions-for-set?]]. Partial permissions means that
the User has permissions for some subpath of the path in question, e.g. Determining query permissionsNormally, a User is allowed to view (i.e., run the query for) a Saved Question if they have read permissions for the Collection in which Saved Question lives, or if they have data permissions for the Database and Table(s) the Question accesses. The main idea here is that some Users with more permissions can go create a curated set of Saved Questions they deem appropriate for less-privileged Users to see, and put them in a Collection they can see. These Users would still be prevented from poking around things on their own, however. The Query Processor middleware in [[metabase.query-processor.middleware.permissions]], [[metabase-enterprise.sandbox.query-processor.middleware.row-level-restrictions]], and [[metabase-enterprise.advanced-permissions.models.permissions.block-permissions]] determines whether the current User has permissions to run the current query. Permissions are as follows: | Data perms? | Coll perms? | Block? | Segmented? | Can run? | | ----------- | ----------- | ------ | ---------- | -------- | | no | no | no | no | ⛔ | | no | no | no | yes | ⚠️ | | no | no | yes | no | ⛔ | | no | no | yes | yes | ⚠️ | | no | yes | no | no | ✅ | | no | yes | no | yes | ⚠️ | | no | yes | yes | no | ⛔ | | no | yes | yes | yes | ⚠️ | | yes | no | no | no | ✅ | | yes | no | no | yes | ✅ | | yes | no | yes | no | ✅ | | yes | no | yes | yes | ✅ | | yes | yes | no | no | ✅ | | yes | yes | no | yes | ✅ | | yes | yes | yes | no | ✅ | | yes | yes | yes | yes | ✅ | ( Known Permissions PathsSee [[path-regex-v1]] for an always-up-to-date list of permissions paths. /collection/:id/ ; read-write perms for a Coll and its non-Coll children /collection/:id/read/ ; read-only perms for a Coll and its non-Coll children /collection/root/ ; read-write perms for the Root Coll and its non-Coll children /colllection/root/read/ ; read-only perms for the Root Coll and its non-Coll children /collection/namespace/:namespace/root/ ; read-write perms for the Root Coll of a non-default namespace (e.g. SQL Snippets) /collection/namespace/:namespace/root/read/ ; read-only perms for the Root Coll of a non-default namespace (e.g. SQL Snippets) /db/:id/ ; full perms for a Database /db/:id/native/ ; ad-hoc native query perms for a Database /db/:id/schema/ ; ad-hoc MBQL query perms for all schemas in DB (does not include native queries) /db/:id/schema/:name/ ; ad-hoc MBQL query perms for a specific schema /db/:id/schema/:name/table/:id/ ; full perms for a Table /db/:id/schema/:name/table/:id/read/ ; perms to fetch info about this Table from the DB /db/:id/schema/:name/table/:id/query/ ; ad-hoc MBQL query perms for a Table /db/:id/schema/:name/table/:id/query/segmented/ ; allow ad-hoc MBQL queries. Sandbox all queries against this Table. /block/db/:id/ ; disallow queries against this DB unless User has data perms. / ; full root perms | (ns metabase.models.permissions (:require [clojure.string :as str] [metabase.audit :as audit] [metabase.config :as config] [metabase.models.interface :as mi] [metabase.models.permissions-group :as perms-group] [metabase.permissions.util :as perms.u] [metabase.plugins.classloader :as classloader] [metabase.public-settings.premium-features :as premium-features :refer [defenterprise]] [metabase.util :as u] [metabase.util.honey-sql-2 :as h2x] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [metabase.util.performance :as perf] [methodical.core :as methodical] [toucan2.core :as t2])) |
+----------------------------------------------------------------------------------------------------------------+ | UTIL FNS | +----------------------------------------------------------------------------------------------------------------+ | |
-------------------------------------------------- Dynamic Vars -------------------------------------------------- | |
Should we allow permissions entries like | (def ^:dynamic ^Boolean *allow-root-entries* false) |
Should we allow changes to be made to permissions belonging to the Admin group? By default this is disabled to
prevent accidental tragedy, but you can enable it here when creating the default entry for | (def ^:dynamic ^Boolean *allow-admin-permissions-changes* false) |
--------------------------------------------------- Assertions --------------------------------------------------- | |
Check to make sure the | (defn- assert-not-admin-group [{:keys [group_id]}] (when (and (= group_id (:id (perms-group/admin))) (not *allow-admin-permissions-changes*)) (throw (ex-info (tru "You cannot create or revoke permissions for the ''Admin'' group.") {:status-code 400})))) |
Check to make sure the value of | (defn- assert-valid-object [{:keys [object]}] (when (and object (not (perms.u/valid-path? object)) (or (not= object "/") (not *allow-root-entries*))) (throw (ex-info (tru "Invalid permissions object path: ''{0}''." object) {:status-code 400, :path object})))) |
Check to make sure this | (defn- assert-valid [permissions] (doseq [f [assert-not-admin-group assert-valid-object]] (f permissions))) |
------------------------------------------------- Path Util Fns -------------------------------------------------- | |
(def ^:private MapOrID [:or :map ms/PositiveInt]) | |
(mu/defn collection-readwrite-path :- perms.u/PathSchema "Return the permissions path for *readwrite* access for a `collection-or-id`." [collection-or-id :- MapOrID] (if-not (get collection-or-id :metabase.models.collection.root/is-root?) (format "/collection/%d/" (u/the-id collection-or-id)) (if-let [collection-namespace (:namespace collection-or-id)] (format "/collection/namespace/%s/root/" (perms.u/escape-path-component (u/qualified-name collection-namespace))) "/collection/root/"))) | |
(mu/defn collection-read-path :- perms.u/PathSchema "Return the permissions path for *read* access for a `collection-or-id`." [collection-or-id :- MapOrID] (str (collection-readwrite-path collection-or-id) "read/")) | |
(mu/defn application-perms-path :- perms.u/PathSchema "Returns the permissions path for *full* access a application permission." [perm-type] (case perm-type :setting "/application/setting/" :monitoring "/application/monitoring/" :subscription "/application/subscription/")) | |
-------------------------------------------- Permissions Checking Fns -------------------------------------------- | |
Does | (defn is-permissions-for-object? [permissions-path path] (str/starts-with? path permissions-path)) |
Does | (defn is-partial-permissions-for-object? [permissions-path path] (or (is-permissions-for-object? permissions-path path) (str/starts-with? permissions-path path))) |
Does | (defn set-has-full-permissions? ^Boolean [permissions-set path] (boolean (perf/some #(is-permissions-for-object? % path) permissions-set))) |
Does | (defn set-has-partial-permissions? ^Boolean [permissions-set path] (boolean (perf/some #(is-partial-permissions-for-object? % path) permissions-set))) |
(mu/defn set-has-full-permissions-for-set? :- :boolean "Do the permissions paths in `permissions-set` grant *full* access to all the object paths in `paths-set`?" [permissions-set paths-set] (let [permissions (or (:as-vec (meta permissions-set)) permissions-set)] (every? (partial set-has-full-permissions? permissions) paths-set))) | |
(mu/defn set-has-partial-permissions-for-set? :- :boolean "Do the permissions paths in `permissions-set` grant *partial* access to all the object paths in `paths-set`? (`permissions-set` must grant partial access to *every* object in `paths-set` set)." [permissions-set paths-set] (let [permissions (or (:as-vec (meta permissions-set)) permissions-set)] (every? (partial set-has-partial-permissions? permissions) paths-set))) | |
(mu/defn set-has-application-permission-of-type? :- :boolean "Does `permissions-set` grant *full* access to a application permission of type `perm-type`?" [permissions-set perm-type] (set-has-full-permissions? permissions-set (application-perms-path perm-type))) | |
(mu/defn perms-objects-set-for-parent-collection :- [:set perms.u/PathSchema] "Implementation of `perms-objects-set` for models with a `collection_id`, such as Card, Dashboard, or Pulse. This simply returns the `perms-objects-set` of the parent Collection (based on `collection_id`) or for the Root Collection if `collection_id` is `nil`." ([this read-or-write] (perms-objects-set-for-parent-collection nil this read-or-write)) ([collection-namespace :- [:maybe ms/KeywordOrString] this :- [:map [:collection_id [:maybe ms/PositiveInt]]] read-or-write :- [:enum :read :write]] ;; based on value of read-or-write determine the approprite function used to calculate the perms path (let [path-fn (case read-or-write :read collection-read-path :write collection-readwrite-path) collection-id (:collection_id this)] ;; now pass that function our collection_id if we have one, or if not, pass it an object representing the Root ;; Collection #{(path-fn (or collection-id {:metabase.models.collection.root/is-root? true :namespace collection-namespace}))}))) | |
(doto ::use-parent-collection-perms (derive ::mi/read-policy.full-perms-for-perms-set) (derive ::mi/write-policy.full-perms-for-perms-set)) | |
(defmethod mi/perms-objects-set ::use-parent-collection-perms [instance read-or-write] (perms-objects-set-for-parent-collection instance read-or-write)) | |
+----------------------------------------------------------------------------------------------------------------+ | ENTITY + LIFECYCLE | +----------------------------------------------------------------------------------------------------------------+ | |
Used to be the toucan1 model name defined using [[toucan.models/defmodel]], now it's a reference to the toucan2 model name. We'll keep this till we replace all the symbols in our codebase. | (def Permissions :model/Permissions) |
(methodical/defmethod t2/table-name :model/Permissions [_model] :permissions) | |
(derive :model/Permissions :metabase/model) | |
Given a | (defn- maybe-break-out-permission-data [permissions] (let [[match? coll-id-str read?] (re-matches #"^/collection/(\d+)/(read/)?" (:object permissions))] (cond-> permissions match? (assoc :collection_id (parse-long coll-id-str) :perm_type :perms/collection-access :perm_value (if read? :read :read-and-write))))) |
(t2/define-before-insert :model/Permissions [permissions] (u/prog1 (maybe-break-out-permission-data permissions) (assert-valid permissions) (log/debug (u/format-color :green "Granting permissions for group %s: %s" (:group_id permissions) (:object permissions))))) | |
(t2/deftransforms :model/Permissions {:perm_type mi/transform-keyword :perm_value mi/transform-keyword}) | |
(t2/define-before-update :model/Permissions [_] (throw (Exception. (tru "You cannot update a permissions entry! Delete it and create a new one.")))) | |
(t2/define-before-delete :model/Permissions [permissions] (log/debug (u/format-color :red "Revoking permissions for group %s: %s" (:group_id permissions) (:object permissions))) (assert-not-admin-group permissions)) | |
--------------------------------------------------- Helper Fns --------------------------------------------------- | |
If [[metabase.api.common/current-user-permissions-set]] is bound, reset it so it gets recalculated on next use. Called by [[delete-related-permissions!]] and [[grant-permissions!]] below, mostly as a convenience for tests that bind a current user and then grant or revoke permissions for that user without rebinding it. | (defn- clear-current-user-cached-permissions! [] ((requiring-resolve 'metabase.server.middleware.session/clear-current-user-cached-permissions-set!)) nil) |
Delete all 'related' permissions for Suppose we asked this functions to delete related permssions for
In short, it will delete any permissions that contain You can optionally include NOTE: This function is meant for internal usage in this namespace only; use one of the other functions like
| (mu/defn delete-related-permissions! [group-or-id :- [:or :map ms/PositiveInt] path :- perms.u/PathSchema & other-conditions] (let [paths (conj (perms.u/->v2-path path) path) where {:where (apply list :and [:= :group_id (u/the-id group-or-id)] (into [:or [:like path (h2x/concat :object (h2x/literal "%"))]] (map (fn [path-form] [:like :object (str path-form "%")]) paths)) other-conditions)}] (when-let [revoked (t2/select-fn-set :object Permissions where)] (log/debug (u/format-color 'red "Revoking permissions for group %d: %s" (u/the-id group-or-id) revoked)) (t2/delete! Permissions where) (clear-current-user-cached-permissions!)))) |
Grant permissions for | (defn grant-permissions! ([group-or-id path] (try (t2/insert! Permissions (map (fn [path-object] {:group_id (u/the-id group-or-id) :object path-object}) (distinct (conj (perms.u/->v2-path path) path)))) (clear-current-user-cached-permissions!) ;; on some occasions through weirdness we might accidentally try to insert a key that's already been inserted (catch Throwable e (log/error e (u/format-color 'red "Failed to grant permissions")) ;; if we're running tests, we're doing something wrong here if duplicate permissions are getting assigned, ;; mostly likely because tests aren't properly cleaning up after themselves, and possibly causing other tests ;; to pass when they shouldn't. Don't allow this during tests (when config/is-test? (throw e)))))) |
Audit Permissions helper fns | |
SQL clause to filter namespaces depending on if audit app is enabled or not, and if the namespace is the default one. | (defn audit-namespace-clause [namespace-keyword namespace-val] (if (and (nil? namespace-val) (premium-features/enable-audit-app?)) [:or [:= namespace-keyword nil] [:= namespace-keyword "analytics"]] [:= namespace-keyword namespace-val])) |
Audit instances should only be readable if audit app is enabled. | (defn can-read-audit-helper [model instance] (if (and (not (premium-features/enable-audit-app?)) (case model :model/Collection (audit/is-collection-id-audit? (:id instance)) (audit/is-parent-collection-audit? instance))) false (case model :model/Collection (mi/current-user-has-full-permissions? :read instance) (mi/current-user-has-full-permissions? (perms-objects-set-for-parent-collection instance :read))))) |
Remove all permissions entries for a Group to access a Application permisisons | (defn revoke-application-permissions! [group-or-id perm-type] (delete-related-permissions! group-or-id (application-perms-path perm-type))) |
Grant full permissions for a group to access a Application permisisons. | (defn grant-application-permissions! [group-or-id perm-type] (grant-permissions! group-or-id (application-perms-path perm-type))) |
(defn- is-personal-collection-or-descendant-of-one? [collection] (classloader/require 'metabase.models.collection) ((resolve 'metabase.models.collection/is-personal-collection-or-descendant-of-one?) collection)) | |
(defn- is-trash-or-descendant? [collection] (classloader/require 'metabase.models.collection) ((resolve 'metabase.models.collection/is-trash-or-descendant?) collection)) | |
(defn- ^:private collection-or-id->collection [collection-or-id] (if (map? collection-or-id) collection-or-id (t2/select-one :model/Collection :id (u/the-id collection-or-id)))) | |
Check whether | (mu/defn- check-is-modifiable-collection [collection-or-id :- MapOrID] ;; skip the whole thing for the root collection, we know it's not a personal collection, trash, or descendant of one ;; of them. (when-not (:metabase.models.collection.root/is-root? collection-or-id) (let [collection (collection-or-id->collection collection-or-id)] ;; Check whether the collection is the Trash collection or a descendant thereof; if so, throw an Exception. This ;; is done because you can't modify the permissions of things in the Trash, you need to untrash them first. (when (is-trash-or-descendant? collection) (throw (ex-info (tru "You cannot edit permissions for the Trash collection or its descendants.") {}))) ;; Check whether the collection is a personal collection or a descendant thereof; if so, throw an Exception. ;; This is done because we *should* never be editing granting/etc. permissions for *Personal* Collections to ;; entire Groups! Their owner will get implicit permissions automatically, and of course admins will be able to ;; see them,but a whole group should never be given some sort of access. (when (is-personal-collection-or-descendant-of-one? collection) (throw (ex-info (tru "You cannot edit permissions for a Personal Collection or its descendants.") {})))))) |
Revoke all access for | (mu/defn revoke-collection-permissions! [group-or-id :- MapOrID collection-or-id :- MapOrID] (check-is-modifiable-collection collection-or-id) (delete-related-permissions! group-or-id (collection-readwrite-path collection-or-id))) |
Grant full access to a Collection, which means a user can view all Cards in the Collection and add/remove Cards. | (mu/defn grant-collection-readwrite-permissions! [group-or-id :- MapOrID collection-or-id :- MapOrID] (check-is-modifiable-collection collection-or-id) (grant-permissions! (u/the-id group-or-id) (collection-readwrite-path collection-or-id))) |
Grant read access to a Collection, which means a user can view all Cards in the Collection. | (mu/defn grant-collection-read-permissions! [group-or-id :- MapOrID collection-or-id :- MapOrID] (check-is-modifiable-collection collection-or-id) (grant-permissions! (u/the-id group-or-id) (collection-read-path collection-or-id))) |
Check if | (defenterprise current-user-has-application-permissions? metabase-enterprise.advanced-permissions.common [_instance] false) |