(ns metabase-enterprise.sandbox.models.params.field-values
(:require
[metabase-enterprise.impersonation.core :as impersonation]
[metabase-enterprise.sandbox.api.table :as table]
[metabase-enterprise.sandbox.query-processor.middleware.row-level-restrictions
:as row-level-restrictions]
[metabase.api.common :as api]
[metabase.lib.util.match :as lib.util.match]
[metabase.models.field :as field]
[metabase.models.field-values :as field-values]
[metabase.models.params.field-values :as params.field-values]
[metabase.premium-features.core :refer [defenterprise]]
[metabase.util :as u]
[toucan2.core :as t2])) | |
(comment api/keep-me) | |
Check if a field is sandboxed. | (defn field-is-sandboxed?
[{:keys [table], :as field}]
;; slight optimization: for the `field-id->field-values` version we can batched hydrate `:table` to avoid having to
;; make a bunch of calls to fetch Table. For `get-or-create-field-values` we don't hydrate `:table` so we can fall
;; back to fetching it manually with `field/table`
(table/only-sandboxed-perms? (or table (field/table field)))) |
Find the GTAP for current user that apply to table | (defn- table-id->gtap
[table-id]
(let [group-ids (t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id api/*current-user-id*)
gtaps (t2/select :model/GroupTableAccessPolicy
:group_id [:in group-ids]
:table_id table-id)]
(when gtaps
(row-level-restrictions/assert-one-gtap-per-table gtaps)
;; there shold be only one gtap per table and we only need one table here
;; see docs in [[metabase.permissions.models.permissions]] for more info
(t2/hydrate (first gtaps) :card)))) |
Returns the gtap attributes for current user that applied to The gtap-attributes is a list with 2 elements:
1. card-id - for GTAP that use a saved question
2. the timestamp when the saved question was last updated
3. a map:
if query is mbql query:
- with key is the user-attribute that applied to the table that For example we have an GTAP rules {:card_id 1 ;; a mbql query :attribute_remappings {"State" [:dimension [:field 3 nil]]}} And users with login-attributes {"State" "CA"} ;; (field-id->gtap-attributes-for-current-user (t2/select-one Field :id 3)) ;; -> [1, {"State" "CA"}] | (defn- field->gtap-attributes-for-current-user
[{:keys [table_id] :as _field}]
(when-let [gtap (table-id->gtap table_id)]
(let [login-attributes (:login_attributes @api/*current-user*)
attribute_remappings (:attribute_remappings gtap)
field-ids (t2/select-fn-set :id :model/Field :table_id table_id)]
[(:card_id gtap)
(-> gtap :card :updated_at)
(if (= :native (get-in gtap [:card :query_type]))
;; For sandbox that uses native query, we can't narrow down to the exact attribute
;; that affect the current table. So we just hash the whole login-attributes of users.
;; This makes hashing a bit less efficient but it ensures that user get a new hash
;; if they change login attributes
login-attributes
(into {} (for [[k v] attribute_remappings
;; get attribute that map to fields of the same table
:when (contains? field-ids
(lib.util.match/match-one v
;; new style with {:stage-number }
[:dimension [:field field-id _] _] field-id
;; old style without stage number
[:dimension [:field field-id _]] field-id))]
{k (get login-attributes k)})))]))) |
Fetch existing FieldValues for a sequence of | (defenterprise field-id->field-values-for-current-user
:feature :sandboxes
[field-ids]
(let [fields (when (seq field-ids)
(t2/hydrate (t2/select :model/Field :id [:in (set field-ids)]) :table))
{unsandboxed-fields false
sandboxed-fields true} (group-by (comp boolean field-is-sandboxed?) fields)]
(merge
;; use the normal OSS batched implementation for any Fields that aren't subject to sandboxing.
(when (seq unsandboxed-fields)
(params.field-values/default-field-id->field-values-for-current-user
(map u/the-id unsandboxed-fields)))
;; for sandboxed fields, fetch the sandboxed values individually.
(into {} (for [{field-id :id, :as field} sandboxed-fields]
[field-id (select-keys (params.field-values/get-or-create-advanced-field-values! :sandbox field)
[:values :human_readable_values :field_id])]))))) |
Fetch cached FieldValues for a | (defenterprise get-or-create-field-values-for-current-user!*
:feature :sandboxes
[field]
(cond
(field-is-sandboxed? field)
(params.field-values/get-or-create-advanced-field-values! :sandbox field)
;; Impersonation can have row-level security enforced by the database, so we still need to store field values per-user.
;; TODO: only do this for DBs with impersonation in effect
(and api/*current-user-id*
(impersonation/impersonated-user?))
(params.field-values/get-or-create-advanced-field-values! :impersonation field)
:else
(params.field-values/default-get-or-create-field-values-for-current-user! field))) |
Returns a hash-key for linked-filter FieldValues if the field is sandboxed, otherwise fallback to the OSS impl. | (defenterprise hash-key-for-linked-filters
:feature :sandboxes
[field-id constraints]
(let [field (t2/select-one :model/Field :id field-id)]
(if (field-is-sandboxed? field)
(str (hash (concat [field-id
constraints]
(field->gtap-attributes-for-current-user field))))
(field-values/default-hash-key-for-linked-filters field-id constraints)))) |
Returns a hash-key for FieldValues if the field is sandboxed, otherwise fallback to the OSS impl. | (defenterprise hash-key-for-sandbox
:feature :sandboxes
[field-id]
(let [field (t2/select-one :model/Field :id field-id)]
(when (field-is-sandboxed? field)
(str (hash (concat [field-id]
(field->gtap-attributes-for-current-user field))))))) |