Files
metabase/enterprise/backend/src/metabase_enterprise/data_apps/db.clj
2026-10-05 16:11:30 +00:00

255 lines
9.9 KiB
Clojure

(ns metabase-enterprise.data-apps.db
"Application database queries for the data-apps module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[metabase-enterprise.data-apps.schema :as data-apps.schema]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
[toucan2.core :as t2]))
(def non-blob-columns
"Every DataApp column except the raw bundle blob."
[:id :entity_id :name :display_name :description :version :bundle_path :enabled :allowed_hosts
:resource_collection_id :permission_group_id :table_ids :draft :bundle_hash :created_at :updated_at])
(def ^:private non-blob-model
(into [:model/DataApp] non-blob-columns))
(mu/defn data-app
"The DataApp with `data-app-id` without its bundle, or nil."
[data-app-id :- ms/PositiveInt]
(t2/select-one non-blob-model :id data-app-id))
(mu/defn data-app-by-slug
"The DataApp named `slug` without its bundle, or nil."
[slug :- :string]
(t2/select-one non-blob-model :name slug))
(mu/defn draft-by-slug
"The draft DataApp named `slug` without its bundle, or nil."
[slug :- :string]
(t2/select-one non-blob-model :name slug :draft true))
(mu/defn enabled-data-app-by-slug
"The enabled DataApp named `slug` without its bundle, or nil."
[slug :- :string]
(t2/select-one non-blob-model :name slug :enabled true))
(mu/defn data-apps
"Every DataApp without its bundle, ordered by display name; only the enabled ones that aren't drafts when
`available?`."
[available? :- [:maybe :boolean]]
(t2/select non-blob-model
(cond-> {:order-by [[:display_name :asc]]}
available? (assoc :where [:and
[:= :enabled true]
[:= :draft false]]))))
(mu/defn data-app-bundle
"The bundle bytes of the DataApp with `data-app-id`."
[data-app-id :- ms/PositiveInt]
(t2/select-one-fn :bundle [:model/DataApp :bundle] :id data-app-id))
(mu/defn reducible-data-apps-with-bundles
"A reducible of the DataApp rows, bundles included, whose `filter-column` is one of `filter-ids` (every row when
`filter-column` is nil), ordered ascending by `order-columns`."
[filter-column :- [:maybe :keyword]
filter-ids :- [:maybe [:sequential [:maybe [:or :int :string]]]]
order-columns :- [:maybe [:sequential :keyword]]]
(t2/reducible-select [:model/DataApp :*]
(cond-> {}
filter-column (assoc :where [:in filter-column filter-ids])
(seq order-columns) (assoc :order-by (mapv (fn [column] [column :asc]) order-columns)))))
(defn table-database-id
"The database ID for the Table with `table-id`, or nil."
[table-id]
(t2/select-one-fn :db_id :model/Table :id table-id
{:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
(defn existing-table-ids
"The IDs from `table-ids` that belong to existing Tables."
[table-ids]
(if (seq table-ids)
(t2/select-pks-set :model/Table :id [:in table-ids]
{:from [(warehouse-schema-overlay/table-query {:user-settings? false})]})
#{}))
(defn users-for-permission-warnings
"The fields needed to calculate permission warnings for Users with `user-ids`."
[user-ids]
(t2/select [:model/User :id :is_superuser :is_active :tenant_id] :id [:in user-ids]))
(defn metrics-by-ids
"The metric Cards with `metric-ids`."
[metric-ids]
(if (seq metric-ids)
(t2/select :model/Card :id [:in metric-ids] :type "metric")
[]))
(mu/defn data-app-exists?
"Whether a DataApp named `slug` exists."
[slug :- :string]
(t2/exists? :model/DataApp :name slug))
(mu/defn insert-data-app!
"Insert the DataApp `row`."
[row :- ::data-apps.schema/data-app.update]
(t2/insert! :model/DataApp row))
(mu/defn insert-data-app-returning-pk! :- ms/PositiveInt
"Insert the DataApp `row`, returning its ID."
[row :- ::data-apps.schema/data-app.update]
(t2/insert-returning-pk! :model/DataApp row))
(mu/defn update-data-app!
"Apply `changes` to the DataApp with `data-app-id`."
[data-app-id :- ms/PositiveInt
changes :- ::data-apps.schema/data-app.update]
(t2/update! :model/DataApp :id data-app-id changes))
(mu/defn delete-data-app!
"Delete the DataApp with `data-app-id`."
[data-app-id :- ms/PositiveInt]
(t2/delete! :model/DataApp :id data-app-id))
(defn permission-group
"The permission group with `group-id`, or nil."
[group-id]
(t2/select-one :model/PermissionsGroup :id group-id))
(defn insert-permission-group!
"Insert a permission group and return it."
[row]
(t2/insert-returning-instance! :model/PermissionsGroup row))
(defn update-permission-group!
"Apply `changes` to the permission group with `group-id`."
[group-id changes]
(t2/update! :model/PermissionsGroup :id group-id changes))
(defn delete-permission-group!
"Delete the permission group with `group-id`."
[group-id]
(t2/delete! :model/PermissionsGroup :id group-id))
(defn data-app-group-ids
"The IDs of permission groups owned by data apps."
[]
(t2/select-pks-set :model/PermissionsGroup :is_data_app_group true))
(defn resource-collection-ids
"The IDs of the resource collections owned by data apps."
[]
(t2/select-fn-set :resource_collection_id :model/DataApp :resource_collection_id [:not= nil]))
(defn databases-with-legacy-permissions
"Database IDs with legacy View Data permissions from groups not owned by apps."
[database-ids]
(if (seq database-ids)
(t2/select-fn-set :db_id :model/DataPermissions
{:select [:p.db_id]
:from [[:data_permissions :p]]
:join [[:permissions_group :g] [:= :g.id :p.group_id]]
:where [:and
[:in :p.db_id database-ids]
[:= :p.perm_type "perms/view-data"]
[:= :p.perm_value "legacy-no-self-service"]
[:= :g.is_data_app_group false]]})
#{}))
(defn resource-collection
"The resource collection with `collection-id`, or nil."
[collection-id]
(t2/select-one :model/Collection :id collection-id))
(defn insert-resource-collection!
"Insert a resource collection and return it."
[row]
(t2/insert-returning-instance! :model/Collection row))
(defn update-resource-collection!
"Apply `changes` to the resource collection with `collection-id`."
[collection-id changes]
(t2/update! :model/Collection :id collection-id changes))
(defn delete-resource-collection!
"Delete the resource collection with `collection-id`."
[collection-id]
(t2/delete! :model/Collection :id collection-id))
(defn non-router-database-ids
"The IDs of databases that are not routed through another database."
[]
(t2/select-pks-set :model/Database :router_database_id nil))
(defn permissions-for-paths-excluding-group
"Permission grants for `paths`, excluding `group-id`."
[paths group-id]
(t2/select [:model/Permissions :group_id :object]
:object [:in paths]
:group_id [:not= group-id]))
(defn table-details
"Table names and database details for `table-ids`."
[table-ids]
(t2/select :model/Table
{:select [:t.id
[:t.display_name :name]
:t.schema
[:t.db_id :database_id]
[:d.name :database_name]]
:from [(warehouse-schema-overlay/table-query {:alias :t})]
:join [[:metabase_database :d] [:= :d.id :t.db_id]]
:where [:in :t.id table-ids]
:order-by [[:d.name :asc] [:t.schema :asc] [:t.display_name :asc]]}))
(defn sandboxed-user-table-access
"Sandboxed table access from non-data-app groups for the requested users and tables."
[user-ids table-ids]
(t2/query {:select-distinct [[:pgm.user_id :user_id]
[:s.table_id :table_id]]
:from [[:permissions_group_membership :pgm]]
:join [[:sandboxes :s] [:= :s.group_id :pgm.group_id]
[:permissions_group :pg] [:= :pg.id :pgm.group_id]]
:where [:and
[:in :pgm.user_id user-ids]
[:in :s.table_id table-ids]
[:not :pg.is_data_app_group]]}))
(defn unrestricted-user-table-access
"Unrestricted table access from non-data-app groups for the requested users and tables."
[user-ids table-ids]
(t2/query {:select-distinct [[:pgm.user_id :user_id]
[:t.id :table_id]]
:from [[:permissions_group_membership :pgm]]
:join [[:data_permissions :dp] [:= :dp.group_id :pgm.group_id]
[:permissions_group :pg] [:= :pg.id :pgm.group_id]
(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})
[:and
[:= :t.db_id :dp.db_id]
[:or
[:= :dp.table_id nil]
[:= :dp.table_id :t.id]]]]
:where [:and
[:in :pgm.user_id user-ids]
[:in :t.id table-ids]
[:not :pg.is_data_app_group]
[:= :dp.perm_type (u/qualified-name :perms/view-data)]
[:= :dp.perm_value "unrestricted"]]}))
(defn active-group-members
"Active user memberships for `group-ids`, including API-key users."
[group-ids]
(t2/query {:select [[:pgm.group_id :group_id]
[:u.id :id]
:u.is_superuser
:u.email]
:from [[:permissions_group_membership :pgm]]
:join [[:core_user :u] [:= :u.id :pgm.user_id]]
:where [:and
[:in :pgm.group_id group-ids]
[:= :u.is_active true]]}))