diff --git a/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj b/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj index 30c60503207523bee2d07fb0f0c76fd442d30001..07616a89b4157d519618b0b593a7517b17d48fd4 100644 --- a/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj +++ b/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj @@ -9,7 +9,8 @@ [metabase.models.field-values :as field-values] [metabase.models.params.field-values :as params.field-values] [metabase.public-settings.premium-features :refer [defenterprise]] - [toucan.db :as db])) + [toucan.db :as db] + [toucan.hydrate :refer [hydrate]])) (comment api/keep-me) @@ -32,7 +33,7 @@ (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.models.permissions]] for more info - (first gtaps)))) + (hydrate (first gtaps) :card)))) (defn- field->gtap-attributes-for-current-user "Returns the gtap attributes for current user that applied to `field`. @@ -40,11 +41,13 @@ The gtap-attributes is a list with 2 elements: 1. card-id - for GTAP that use a saved question 2. a map: - - with key is the user-attribute that applied to the table that `field` is in - - value is the user-attribute of current user corresponding to the key + if query is mbql query: + - with key is the user-attribute that applied to the table that `field` is in + - value is the user-attribute of current user corresponding to the key + for native query, this map will be the login-attributes of user For example we have an GTAP rules - {:card_id 1 + {:card_id 1 ;; a mbql query :attribute_remappings {\"State\" [:dimension [:field 3 nil]]}} And users with login-attributes {\"State\" \"CA\"} @@ -57,11 +60,17 @@ attribute_remappings (:attribute_remappings gtap) field-ids (db/select-field :id Field :table_id table_id)] [(:card_id gtap) - (into {} (for [[k v] attribute_remappings - ;; get attribute that map to fields of the same table - :when (contains? field-ids - (mbql.u/match-one v [:dimension [:field field-id _]] field-id))] - {k (get login-attributes k)}))]))) + (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 + (mbql.u/match-one v [:dimension [:field field-id _]] field-id))] + {k (get login-attributes k)})))]))) (defenterprise get-or-create-field-values-for-current-user!* "Fetch cached FieldValues for a `field`, creating them if needed if the Field should have FieldValues. These diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/api/field_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/api/field_test.clj index 478d32f3eade75cb6364d12175219f79d0cb6b8a..d280eab60405df73ab581fb55fd0919a85994f14 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/api/field_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/api/field_test.clj @@ -22,67 +22,82 @@ (deftest field-values-test (testing "GET /api/field/:id/values" - (mt/with-gtaps {:gtaps {:venues {:query (mt.tu/restricted-column-query (mt/id)) - :remappings {:cat [:dimension (mt/id :venues :category_id)]}}} - :attributes {:cat 50}} - (testing (str "When I call the FieldValues API endpoint for a Field that I have segmented table access only " - "for, will I get ad-hoc values?\n") - (letfn [(fetch-values [user field] - (-> (mt/user-http-request user :get 200 (format "field/%d/values" (mt/id :venues field))) - (update :values (partial take 3))))] - ;; Rasta Toucan is only allowed to see Venues that are in the "Mexican" category [category_id = 50]. So - ;; fetching FieldValues for `venue.name` should do an ad-hoc fetch and only return the names of venues in - ;; that category. - (is (= {:field_id (mt/id :venues :name) - :values [["Garaje"] - ["Gordo Taqueria"] - ["La Tortilla"]] - :has_more_values false} - (fetch-values :rasta :name))) + (mt/with-temp-copy-of-db + (doseq [[query-type gtap-rule] + [["MBQL" + {:gtaps {:venues {:query (mt.tu/restricted-column-query (mt/id)) + :remappings {:cat [:dimension (mt/id :venues :category_id)]}}} + :attributes {:cat 50}}] + ["native" + {:gtaps {:venues {:query + (mt/native-query + {:query "SELECT id, name, category_id FROM venues WHERE category_id = {{cat}}" + :template-tags {"cat" {:id "__MY_CAT__" + :name "cat" + :display-name "Cat id" + :type :number}}}) + :remappings {:cat [:variable [:template-tag "cat"]]}}} + :attributes {:cat 50}}]]] + (testing (format "GTAP rule is a %s query" query-type) + (mt/with-gtaps gtap-rule + (testing (str "When I call the FieldValues API endpoint for a Field that I have segmented table access only " + "for, will I get ad-hoc values?\n") + (letfn [(fetch-values [user field] + (-> (mt/user-http-request user :get 200 (format "field/%d/values" (mt/id :venues field))) + (update :values (partial take 3))))] + ;; Rasta Toucan is only allowed to see Venues that are in the "Mexican" category [category_id = 50]. n + ;; fetching FieldValues for `venue.name` should do an ad-hoc fetch and only return the names of venues in + ;; that category. + (is (= {:field_id (mt/id :venues :name) + :values [["Garaje"] + ["Gordo Taqueria"] + ["La Tortilla"]] + :has_more_values false} + (fetch-values :rasta :name))) - (testing (str "Now in this case recall that the `restricted-column-query` GTAP we're using does *not* include " - "`venues.price` in the results. (Toucan isn't allowed to know the number of dollar signs!) So " - "make sure if we try to fetch the field values instead of seeing `[[1] [2] [3] [4]]` we get no " - "results") - (is (= {:field_id (mt/id :venues :price) - :values [] - :has_more_values false} - (fetch-values :rasta :price)))) + (testing (str "Now in this case recall that the `restricted-column-query` GTAP we're using does *not* include " + "`venues.price` in the results. (Toucan isn't allowed to know the number of dollar signs!) So " + "make sure if we try to fetch the field values instead of seeing `[[1] [2] [3] [4]]` we get no " + "results") + (is (= {:field_id (mt/id :venues :price) + :values [] + :has_more_values false} + (fetch-values :rasta :price)))) - (testing "Reset field values; if another User fetches them first, do I still see sandboxed values? (metabase/metaboat#128)" - (field-values/clear-field-values-for-field! (mt/id :venues :name)) - ;; fetch Field values with an admin - (testing "Admin should see all Field values" - (is (= {:field_id (mt/id :venues :name) - :values [["20th Century Cafe"] - ["25°"] - ["33 Taps"]] - :has_more_values false} - (fetch-values :crowberto :name)))) - (testing "Sandboxed User should still see only their values after an admin fetches the values" - (is (= {:field_id (mt/id :venues :name) - :values [["Garaje"] - ["Gordo Taqueria"] - ["La Tortilla"]] - :has_more_values false} - (fetch-values :rasta :name)))) - (testing "A User with a *different* sandbox should see their own values" - (let [password (mt/random-name)] - (mt/with-temp User [another-user {:password password}] - (mt/with-gtaps-for-user another-user {:gtaps {:venues - {:remappings - {:cat - [:dimension (mt/id :venues :category_id)]}}} - :attributes {:cat 5 #_BBQ}} + (testing "Reset field values; if another User fetches them first, do I still see sandboxed values? (metabase/metaboat#128)" + (field-values/clear-field-values-for-field! (mt/id :venues :name)) + ;; fetch Field values with an admin + (testing "Admin should see all Field values" + (is (= {:field_id (mt/id :venues :name) + :values [["20th Century Cafe"] + ["25°"] + ["33 Taps"]] + :has_more_values false} + (fetch-values :crowberto :name)))) + (testing "Sandboxed User should still see only their values after an admin fetches the values" (is (= {:field_id (mt/id :venues :name) - :values [["Baby Blues BBQ"] - ["Bludso's BBQ"] - ["Boneyard Bistro"]] + :values [["Garaje"] + ["Gordo Taqueria"] + ["La Tortilla"]] :has_more_values false} - (-> (mt/client {:username (:email another-user), :password password} - :get 200 - (format "field/%d/values" (mt/id :venues :name))) - (update :values (partial take 3))))))))))))))) + (fetch-values :rasta :name)))) + (testing "A User with a *different* sandbox should see their own values" + (let [password (mt/random-name)] + (mt/with-temp User [another-user {:password password}] + (mt/with-gtaps-for-user another-user {:gtaps {:venues + {:remappings + {:cat + [:dimension (mt/id :venues :category_id)]}}} + :attributes {:cat 5 #_BBQ}} + (is (= {:field_id (mt/id :venues :name) + :values [["Baby Blues BBQ"] + ["Bludso's BBQ"] + ["Boneyard Bistro"]] + :has_more_values false} + (-> (mt/client {:username (:email another-user), :password password} + :get 200 + (format "field/%d/values" (mt/id :venues :name))) + (update :values (partial take 3)))))))))))))))))) (deftest human-readable-values-test (testing "GET /api/field/:id/values should returns correct human readable mapping if exists" diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/models/params/field_values_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/models/params/field_values_test.clj index c326667f22d767c56de1757b1a8ca7ee068d6c7b..67a44199f48cc172fc1ab5c519dae1a6ba345415 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/models/params/field_values_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/models/params/field_values_test.clj @@ -119,6 +119,32 @@ (is (not= (hash-for-user-id-with-attributes user-id-1 {"State" "CA"} (mt/id :categories :name)) (hash-for-user-id-with-attributes user-id-2 {"State" "NY"} (mt/id :categories :name))))))) + (testing "gtap with native question" + (mt/with-temp* + [Card [{card-id :id} {:query_type :native + :name "A native query" + :dataset_query + {:type :native + :database (mt/id) + :native + {:query "SELECT * FROM People WHERE state = {{STATE}}" + :template-tags + {"STATE" {:id "72461b3b-3877-4538-a5a3-7a3041924517" + :name "STATE" + :display-name "STATE" + :type "text"}}}}}] + PermissionsGroup [{group-id :id}] + User [{user-id :id}] + PermissionsGroupMembership [_ {:group_id group-id + :user_id user-id}] + GroupTableAccessPolicy [_ {:card_id card-id + :group_id group-id + :table_id (mt/id :categories) + :attribute_remappings {"State" [:variable [:template-tag "STATE"]]}}]] + (testing "same users but if the login_attributes change, they should have different hash (#24966)" + (is (not= (hash-for-user-id-with-attributes user-id {"State" "CA"} (mt/id :categories :name)) + (hash-for-user-id-with-attributes user-id {"State" "NY"} (mt/id :categories :name))))))) + (testing "2 users in different groups but gtaps use the same card" (mt/with-temp* [Card [{card-id :id}] diff --git a/frontend/test/metabase/scenarios/permissions/reproductions/24966-saved-native-q-field-values.cy.spec.js b/frontend/test/metabase/scenarios/permissions/reproductions/24966-saved-native-q-field-values.cy.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..2150b3a9caf2afd1017db80f1c5a297a8400b1f8 --- /dev/null +++ b/frontend/test/metabase/scenarios/permissions/reproductions/24966-saved-native-q-field-values.cy.spec.js @@ -0,0 +1,121 @@ +import { + restore, + visitQuestion, + visitDashboard, + filterWidget, +} from "__support__/e2e/helpers"; + +import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database"; + +const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE; + +const sandboxingQuestion = { + name: "geadsfasd", + native: { + query: + "select products.category,PRODUCTS.title from PRODUCTS where true [[AND products.CATEGORY = {{category}}]]", + "template-tags": { + category: { + id: "411b40bb-1374-9787-6ffb-20604df56d73", + name: "category", + "display-name": "Category", + type: "text", + }, + }, + }, + parameters: [ + { + id: "411b40bb-1374-9787-6ffb-20604df56d73", + type: "category", + target: ["variable", ["template-tag", "category"]], + name: "Category", + slug: "category", + }, + ], +}; + +const dashboardFilter = { + name: "Text", + slug: "text", + id: "ec00b255", + type: "string/=", + sectionId: "string", +}; + +const dashboardDetails = { parameters: [dashboardFilter] }; + +describe("issue 24966", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + + // Add user attribute to existing ("nodata" / id:3 user + cy.request("PUT", "/api/user/3", { + login_attributes: { attr_cat: "Gizmo" }, + }); + + cy.createNativeQuestion(sandboxingQuestion).then(({ body: { id } }) => { + visitQuestion(id); + + cy.sandboxTable({ + table_id: PRODUCTS_ID, + card_id: id, + attribute_remappings: { + attr_cat: ["variable", ["template-tag", "category"]], + }, + }); + }); + + // Add the saved products table to the dashboard + cy.createQuestionAndDashboard({ + questionDetails: { + query: { + "source-table": PRODUCTS_ID, + limit: 10, + }, + }, + dashboardDetails, + }).then(({ body: { id, card_id, dashboard_id } }) => { + cy.wrap(dashboard_id).as("dashboardId"); + + // Connect the filter to the card + cy.request("PUT", `/api/dashboard/${dashboard_id}/cards`, { + cards: [ + { + id, + card_id, + col: 0, + row: 0, + sizeX: 12, + sizeY: 8, + parameter_mappings: [ + { + parameter_id: dashboardFilter.id, + card_id, + target: ["dimension", ["field", PRODUCTS.CATEGORY, null]], + }, + ], + }, + ], + }); + }); + }); + + it("should correctly fetch field values for a filter when native question is used for sandboxing (metabase#24966)", () => { + cy.get("@dashboardId").then(id => { + cy.signIn("nodata"); + visitDashboard(id); + filterWidget().click(); + cy.findByTestId("Gizmo-filter-value").click(); + cy.button("Add filter").click(); + cy.location("search").should("eq", "?text=Gizmo"); + + cy.signInAsSandboxedUser(); + visitDashboard(id); + filterWidget().click(); + cy.findByTestId("Widget-filter-value").click(); + cy.button("Add filter").click(); + cy.location("search").should("eq", "?text=Widget"); + }); + }); +});