/api/permissions endpoints. | (ns metabase.api.permissions (:require [clojure.data :as data] [compojure.core :refer [DELETE GET POST PUT]] [honey.sql.helpers :as sql.helpers] [java-time.api :as t] [malli.core :as mc] [malli.transform :as mtx] [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.api.permission-graph :as api.permission-graph] [metabase.db :as mdb] [metabase.db.query :as mdb.query] [metabase.models :refer [PermissionsGroupMembership User]] [metabase.models.data-permissions.graph :as data-perms.graph] [metabase.models.interface :as mi] [metabase.models.permissions-group :as perms-group :refer [PermissionsGroup]] [metabase.models.permissions-revision :as perms-revision] [metabase.models.setting :as setting :refer [defsetting]] [metabase.permissions.util :as perms.u] [metabase.public-settings.premium-features :as premium-features :refer [defenterprise]] [metabase.request.core :as request] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru tru]] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) |
v50 Permissions Tutorial settings Bryan (7/8/24): The following 2 settings are meant to mitigate any confusion for admins over the new and improved permissions format. It looks different, so we want to make sure they know what's up. We don't want to show the modal or banner to admins who started using MB after v50 or who have dismissed them. We should come back around and delete them from the master branch in a few months. | |
(defn- instance-create-time [] (->> (t2/select-one [:model/User [:%min.date_joined :min]]) :min t/local-date-time)) | |
(defn- v-fifty-migration-time [] (let [v50-migration-id "v50.2024-01-04T13:52:51"] (->> (mdb/changelog-by-id (mdb/app-db) v50-migration-id) :dateexecuted))) | |
(defsetting show-updated-permission-modal (deferred-tru "Whether an introductory modal should be shown for admins when they first upgrade to the new data-permissions format.") :visibility :admin :export? false :default true :user-local :only :getter (fn [] (if (t/after? (instance-create-time) (v-fifty-migration-time)) false (setting/get-value-of-type :boolean :show-updated-permission-modal))) :type :boolean :audit :never) | |
(defsetting show-updated-permission-banner (deferred-tru "Whether an informational header should be displayed in the permissions editor about the new data-permissions format.") :visibility :admin :export? false :default true :user-local :only :getter (fn [] (if (t/after? (instance-create-time) (v-fifty-migration-time)) false (setting/get-value-of-type :boolean :show-updated-permission-banner))) :type :boolean :audit :never) | |
+----------------------------------------------------------------------------------------------------------------+ | PERMISSIONS GRAPH ENDPOINTS | +----------------------------------------------------------------------------------------------------------------+ | |
--------------------------------------------------- Endpoints ---------------------------------------------------- | |
/graph | (api/defendpoint GET "Fetch a graph of all Permissions." [] (api/check-superuser) (data-perms.graph/api-graph)) |
/graph/db/:db-id | (api/defendpoint GET "Fetch a graph of all Permissions for db-id `db-id`." [db-id] {db-id ms/PositiveInt} (api/check-superuser) (data-perms.graph/api-graph {:db-id db-id})) |
/graph/group/:group-id | (api/defendpoint GET "Fetch a graph of all Permissions for group-id `group-id`." [group-id] {group-id ms/PositiveInt} (api/check-superuser) (data-perms.graph/api-graph {:group-id group-id})) |
OSS implementation of | (defenterprise upsert-sandboxes! metabase-enterprise.sandbox.models.group-table-access-policy [_sandboxes] (throw (premium-features/ee-feature-error (tru "Sandboxes")))) |
OSS implementation of | (defenterprise insert-impersonations! metabase-enterprise.advanced-permissions.models.connection-impersonation [_impersonations] (throw (premium-features/ee-feature-error (tru "Connection impersonation")))) |
/graph | (api/defendpoint PUT "Do a batch update of Permissions by passing in a modified graph. This should return the same graph, in the same format, that you got from `GET /api/permissions/graph`, with any changes made in the wherever necessary. This modified graph must correspond to the `PermissionsGraph` schema. If successful, this endpoint returns the updated permissions graph; use this as a base for any further modifications. Revisions to the permissions graph are tracked. If you fetch the permissions graph and some other third-party modifies it before you can submit you revisions, the endpoint will instead make no changes and return a 409 (Conflict) response. In this case, you should fetch the updated graph and make desired changes to that. The optional `sandboxes` key contains a list of sandboxes that should be created or modified in conjunction with this permissions graph update. Since data sandboxing is an Enterprise Edition-only feature, a 402 (Payment Required) response will be returned if this key is present and the server is not running the Enterprise Edition, and/or the `:sandboxes` feature flag is not present. If the skip-graph query param is truthy, then the graph will not be returned." [:as {body :body {skip-graph :skip-graph force :force} :params}] {body :map skip-graph [:maybe ms/BooleanValue] force [:maybe ms/BooleanValue]} (api/check-superuser) (let [new-graph (mc/decode api.permission-graph/StrictApiPermissionsGraph body (mtx/transformer mtx/string-transformer (mtx/transformer {:name :perm-graph})))] (when-not (mc/validate api.permission-graph/DataPermissionsGraph new-graph) (let [explained (mu/explain api.permission-graph/DataPermissionsGraph new-graph)] (throw (ex-info (tru "Cannot parse permissions graph because it is invalid: {0}" (pr-str explained)) {:status-code 400})))) (t2/with-transaction [_conn] (let [group-ids (-> new-graph :groups keys) old-graph (data-perms.graph/api-graph {:group-ids group-ids}) [old new] (data/diff (:groups old-graph) (:groups new-graph)) old (or old {}) new (or new {})] (perms.u/log-permissions-changes old new) (when-not force (perms.u/check-revision-numbers old-graph new-graph)) (data-perms.graph/update-data-perms-graph! {:groups new}) (perms.u/save-perms-revision! :model/PermissionsRevision (:revision old-graph) old new) (let [sandbox-updates (:sandboxes new-graph) sandboxes (when sandbox-updates (upsert-sandboxes! sandbox-updates)) impersonation-updates (:impersonations new-graph) impersonations (when impersonation-updates (insert-impersonations! impersonation-updates)) group-ids (-> new-graph :groups keys)] (merge {:revision (perms-revision/latest-id)} (when-not skip-graph {:groups (:groups (data-perms.graph/api-graph {:group-ids group-ids}))}) (when sandboxes {:sandboxes sandboxes}) (when impersonations {:impersonations impersonations}))))))) |
+----------------------------------------------------------------------------------------------------------------+ | PERMISSIONS GROUP ENDPOINTS | +----------------------------------------------------------------------------------------------------------------+ | |
Return a map of | (defn- group-id->num-members [] (let [results (mdb.query/query {:select [[:pgm.group_id :group_id] [[:count :pgm.id] :members]] :from [[:permissions_group_membership :pgm]] :left-join [[:core_user :user] [:= :pgm.user_id :user.id]] :where [:= :user.is_active true] :group-by [:pgm.group_id]})] (zipmap (map :group_id results) (map :members results)))) |
Return a sequence of ordered | (defn- ordered-groups [limit offset query] (t2/select PermissionsGroup (cond-> {:order-by [:%lower.name]} (some? limit) (sql.helpers/limit limit) (some? offset) (sql.helpers/offset offset) (some? query) (sql.helpers/where query)))) |
(mi/define-batched-hydration-method add-member-counts :member_count "Efficiently add `:member_count` to PermissionGroups." [groups] (let [group-id->num-members (group-id->num-members)] (for [group groups] (assoc group :member_count (get group-id->num-members (u/the-id group) 0))))) | |
/group | (api/defendpoint GET "Fetch all `PermissionsGroups`, including a count of the number of `:members` in that group. This API requires superuser or group manager of more than one group. Group manager is only available if `advanced-permissions` is enabled and returns only groups that user is manager of." [] (try (validation/check-group-manager) (catch clojure.lang.ExceptionInfo _e (validation/check-has-application-permission :setting))) (let [query (when (and (not api/*is-superuser?*) (premium-features/enable-advanced-permissions?) api/*is-group-manager?*) [:in :id {:select [:group_id] :from [:permissions_group_membership] :where [:and [:= :user_id api/*current-user-id*] [:= :is_group_manager true]]}])] (-> (ordered-groups (request/limit) (request/offset) query) (t2/hydrate :member_count)))) |
/group/:id | (api/defendpoint GET "Fetch the details for a certain permissions group." [id] {id ms/PositiveInt} (validation/check-group-manager id) (api/check-404 (-> (t2/select-one PermissionsGroup :id id) (t2/hydrate :members)))) |
/group | (api/defendpoint POST "Create a new `PermissionsGroup`." [:as {{:keys [name]} :body}] {name ms/NonBlankString} (api/check-superuser) (first (t2/insert-returning-instances! PermissionsGroup :name name))) |
/group/:group-id | (api/defendpoint PUT "Update the name of a `PermissionsGroup`." [group-id :as {{:keys [name]} :body}] {group-id ms/PositiveInt name ms/NonBlankString} (validation/check-manager-of-group group-id) (api/check-404 (t2/exists? PermissionsGroup :id group-id)) (t2/update! PermissionsGroup group-id {:name name}) ;; return the updated group (t2/select-one PermissionsGroup :id group-id)) |
/group/:group-id | (api/defendpoint DELETE "Delete a specific `PermissionsGroup`." [group-id] {group-id ms/PositiveInt} (validation/check-manager-of-group group-id) (t2/delete! PermissionsGroup :id group-id) api/generic-204-no-content) |
------------------------------------------- Group Membership Endpoints ------------------------------------------- | |
/membership | (api/defendpoint GET "Fetch a map describing the group memberships of various users. This map's format is: {<user-id> [{:membership_id <id> :group_id <id> :is_group_manager boolean}]}" [] (validation/check-group-manager) (group-by :user_id (t2/select [PermissionsGroupMembership [:id :membership_id] :group_id :user_id :is_group_manager] (cond-> {} (and (not api/*is-superuser?*) api/*is-group-manager?*) (sql.helpers/where [:in :group_id {:select [:group_id] :from [:permissions_group_membership] :where [:and [:= :user_id api/*current-user-id*] [:= :is_group_manager true]]}]))))) |
/membership | (api/defendpoint POST "Add a `User` to a `PermissionsGroup`. Returns updated list of members belonging to the group." [:as {{:keys [group_id user_id is_group_manager]} :body}] {group_id ms/PositiveInt user_id ms/PositiveInt is_group_manager [:maybe :boolean]} (let [is_group_manager (boolean is_group_manager)] (validation/check-manager-of-group group_id) (when is_group_manager ;; enable `is_group_manager` require advanced-permissions enabled (validation/check-advanced-permissions-enabled :group-manager) (api/check (t2/exists? User :id user_id :is_superuser false) [400 (tru "Admin cant be a group manager.")])) (t2/insert! PermissionsGroupMembership :group_id group_id :user_id user_id :is_group_manager is_group_manager) ;; TODO - it's a bit silly to return the entire list of members for the group, just return the newly created one and ;; let the frontend add it as appropriate (:members (t2/hydrate (t2/instance :model/PermissionsGroup {:id group_id}) :members)))) |
/membership/:id | (api/defendpoint PUT "Update a Permission Group membership. Returns the updated record." [id :as {{:keys [is_group_manager]} :body}] {id ms/PositiveInt is_group_manager :boolean} ;; currently this API is only used to update the `is_group_manager` flag and it requires advanced-permissions (validation/check-advanced-permissions-enabled :group-manager) ;; Make sure only Super user or Group Managers can call this (validation/check-group-manager) (let [old (t2/select-one PermissionsGroupMembership :id id)] (api/check-404 old) (validation/check-manager-of-group (:group_id old)) (api/check (t2/exists? User :id (:user_id old) :is_superuser false) [400 (tru "Admin cant be a group manager.")]) (t2/update! PermissionsGroupMembership (:id old) {:is_group_manager is_group_manager}) (t2/select-one PermissionsGroupMembership :id (:id old)))) |
/membership/:group-id/clear | (api/defendpoint PUT "Remove all members from a `PermissionsGroup`. Returns a 400 (Bad Request) if the group ID is for the admin group." [group-id] {group-id ms/PositiveInt} (validation/check-manager-of-group group-id) (api/check-404 (t2/exists? PermissionsGroup :id group-id)) (api/check-400 (not= group-id (u/the-id (perms-group/admin)))) (t2/delete! PermissionsGroupMembership :group_id group-id) api/generic-204-no-content) |
/membership/:id | (api/defendpoint DELETE "Remove a User from a PermissionsGroup (delete their membership)." [id] {id ms/PositiveInt} (let [membership (t2/select-one PermissionsGroupMembership :id id)] (api/check-404 membership) (validation/check-manager-of-group (:group_id membership)) (t2/delete! PermissionsGroupMembership :id id) api/generic-204-no-content)) |
(api/define-routes) | |