Code for generating and updating the Collection permissions graph. See [[metabase.models.permissions]] for more details and for the code for generating and updating the data permissions graph. | (ns metabase.models.collection.graph (:require [clojure.data :as data] [com.climate.claypoole :as cp] [metabase.api.common :as api] [metabase.audit :as audit] [metabase.db.query :as mdb.query] [metabase.models.collection :as collection :refer [Collection]] [metabase.models.collection-permission-graph-revision :as c-perm-revision] [metabase.models.permissions :as perms :refer [Permissions]] [metabase.models.permissions-group :as perms-group :refer [PermissionsGroup]] [metabase.permissions.util :as perms.u] [metabase.public-settings.premium-features :refer [defenterprise]] [metabase.util :as u] [metabase.util.honey-sql-2 :as h2x] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) |
+----------------------------------------------------------------------------------------------------------------+ | PERMISSIONS GRAPH | +----------------------------------------------------------------------------------------------------------------+ | |
---------------------------------------------------- Schemas ----------------------------------------------------- | |
(def ^:private CollectionPermissions [:enum :write :read :none]) | |
collection-id -> status | (def ^:private GroupPermissionsGraph ; when doing a delta between old graph and new graph root won't always ; be present, which is why it's *optional* [:map-of [:or [:= :root] ms/PositiveInt] CollectionPermissions]) |
(def ^:private PermissionsGraph [:map {:closed true} [:revision {:optional true} [:maybe :int]] [:groups [:map-of ms/PositiveInt GroupPermissionsGraph]]]) | |
-------------------------------------------------- Fetch Graph --------------------------------------------------- | |
(defn- group-id->permissions-set [] (into {} (for [[group-id perms] (group-by :group_id (t2/select Permissions))] {group-id (set (map :object perms))}))) | |
(mu/defn- perms-type-for-collection :- CollectionPermissions [permissions-set collection-or-id] (cond (perms/set-has-full-permissions? permissions-set (perms/collection-readwrite-path collection-or-id)) :write (perms/set-has-full-permissions? permissions-set (perms/collection-read-path collection-or-id)) :read :else :none)) | |
(mu/defn- group-permissions-graph :- GroupPermissionsGraph "Return the permissions graph for a single group having `permissions-set`." [collection-namespace permissions-set collection-ids] (into {:root (perms-type-for-collection permissions-set (assoc collection/root-collection :namespace collection-namespace))} (for [collection-id collection-ids] {collection-id (perms-type-for-collection permissions-set collection-id)}))) | |
(mu/defn- non-personal-collection-ids :- [:set ms/PositiveInt] "Return a set of IDs of all Collections that are neither Personal Collections nor descendants of Personal Collections (i.e., things that you can set Permissions for, and that should go in the graph.)" [collection-namespace :- [:maybe ms/KeywordOrString]] (let [personal-collection-ids (t2/select-pks-set Collection :personal_owner_id [:not= nil]) honeysql-form {:select [[:id :id]] :from [:collection] :where (into [:and ;; Does 'NULL != "trash"'? Postgres says the answer is undefined, aka ;; NULL, which... is falsey. :sob: [:or [:= :type nil] [:not= :type "trash"]] (perms/audit-namespace-clause :namespace (u/qualified-name collection-namespace)) [:= :personal_owner_id nil]] (for [collection-id personal-collection-ids] [:not [:like :location (h2x/literal (format "/%d/%%" collection-id))]]))}] (set (map :id (mdb.query/query honeysql-form))))) | |
(defn- calculate-perm-groups [collection-namespace group-id->perms collection-ids] (into {} #_:clj-kondo/ignore (cp/with-shutdown! [pool (+ 2 (cp/ncpus))] (doall (cp/upmap pool (fn [group-id] [group-id (group-permissions-graph collection-namespace (group-id->perms group-id) collection-ids)]) (t2/select-pks-set PermissionsGroup)))))) | |
Return the permission graph for the collections with id in | (defn- collection-permission-graph ([collection-ids] (collection-permission-graph collection-ids nil)) ([collection-ids collection-namespace] (let [group-id->perms (group-id->permissions-set)] {:revision (c-perm-revision/latest-id) :groups (calculate-perm-groups collection-namespace group-id->perms collection-ids)}))) |
In the graph, override the instance analytics collection within the admin group to read. | (defn- modify-instance-analytics-for-admins [graph] (let [admin-group-id (:id (perms-group/admin)) audit-collection-id (:id (audit/default-audit-collection))] (if (nil? audit-collection-id) graph (assoc-in graph [:groups admin-group-id audit-collection-id] :read)))) |
(mu/defn graph :- PermissionsGraph "Fetch a graph representing the current permissions status for every group and all permissioned collections. This works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that function. The graph is restricted to a given namespace by the optional `collection-namespace` param; by default, `nil`, which restricts it to the 'default' namespace containing normal Card/Dashboard/Pulse Collections. Note: All Collections are returned at the same level of the 'graph', regardless of how the Collection hierarchy is structured. Collections do not inherit permissions from ancestor Collections in the same way data permissions are inherited (e.g. full `:read` perms for a Database implies `:read` perms for all its schemas); a 'child' object (e.g. schema) *cannot* have more restrictive permissions than its parent (e.g. Database). Child Collections *can* have more restrictive permissions than their parent." ([] (graph nil)) ([collection-namespace :- [:maybe ms/KeywordOrString]] (t2/with-transaction [_conn] (-> collection-namespace non-personal-collection-ids (collection-permission-graph collection-namespace) modify-instance-analytics-for-admins)))) | |
-------------------------------------------------- Update Graph -------------------------------------------------- | |
Update the permissions for group ID with | (mu/defn- update-collection-permissions! [collection-namespace :- [:maybe ms/KeywordOrString] group-id :- ms/PositiveInt collection-id :- [:or [:= :root] ms/PositiveInt] new-collection-perms :- CollectionPermissions] (let [collection-id (if (= collection-id :root) (assoc collection/root-collection :namespace collection-namespace) collection-id)] ;; remove whatever entry is already there (if any) and add a new entry if applicable (perms/revoke-collection-permissions! group-id collection-id) (case new-collection-perms :write (perms/grant-collection-readwrite-permissions! group-id collection-id) :read (perms/grant-collection-read-permissions! group-id collection-id) :none nil))) |
(mu/defn- update-group-permissions! [collection-namespace :- [:maybe ms/KeywordOrString] group-id :- ms/PositiveInt new-group-perms :- GroupPermissionsGraph] (doseq [[collection-id new-perms] new-group-perms] (update-collection-permissions! collection-namespace group-id collection-id new-perms))) | |
OSS implementation of | (defenterprise update-audit-collection-permissions! metabase-enterprise.audit-app.permissions [_ _] ::noop) |
Increments the current revision number and writes it to the database. This lets us track the permissions graph revision number, which is used for consistency checks when updating the graph. | (defn create-perms-revision! [current-revision-number] (when api/*current-user-id* (first (t2/insert-returning-instances! :model/CollectionPermissionGraphRevision :id (inc current-revision-number) :user_id api/*current-user-id* :before "" :after "")))) |
Updates perm revision, this is used for logging/auditing purposes, and can be quite expensive, so in practice is called after the revision number is updated. | (defn fill-revision-details! [revision-id before changes] (future (t2/update! :model/CollectionPermissionGraphRevision revision-id {:before before :after changes}))) |
Update the Collections permissions graph for Collections of If there are no changes, returns nil.
If there are changes, returns the future that is used to call | (mu/defn update-graph! ([new-graph] (update-graph! nil new-graph false)) ([collection-namespace :- [:maybe ms/KeywordOrString] new-graph :- PermissionsGraph force? :- [:maybe boolean?]] (let [old-graph (graph collection-namespace) old-perms (:groups old-graph) new-perms (:groups new-graph) ;; filter out any groups not in the old graph new-perms (select-keys new-perms (keys old-perms)) ;; filter out any collections not in the old graph new-perms (into {} (for [[group-id collection-id->perms] new-perms] [group-id (select-keys collection-id->perms (keys (get old-perms group-id)))])) [diff-old changes] (data/diff old-perms new-perms)] (when-not force? (perms.u/check-revision-numbers old-graph new-graph)) (when (seq changes) (let [revision-id (t2/with-transaction [_conn] (doseq [[group-id changes] changes] (update-audit-collection-permissions! group-id changes) (update-group-permissions! collection-namespace group-id changes)) (:id (create-perms-revision! (:revision old-graph))))] ;; The graph is updated infrequently, but `diff-old` and `old-graph` can get huge on larger instances. (perms.u/log-permissions-changes diff-old changes) (fill-revision-details! revision-id (assoc old-graph :namespace collection-namespace) changes)))))) |