From ed3f3ae7567a2a343cffcc6fd59aeee95660cfee Mon Sep 17 00:00:00 2001 From: "metabase-bot[bot]" <109303359+metabase-bot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 08:05:09 +0100 Subject: [PATCH] Make chain filters viewable when collection permissions allow it (#28581) (#28766) --- .../sandbox/api/dashboard_test.clj | 24 +++++----- ...ta-should-use-dashboard-filters.cy.spec.js | 32 +++++++++---- .../dashboard_data_permissions.cy.spec.js | 18 +++++++- src/metabase/api/dashboard.clj | 33 +++++++------- src/metabase/api/field.clj | 3 +- src/metabase/models/params/custom_values.clj | 8 ++-- .../middleware/permissions.clj | 20 +++++++-- test/metabase/api/dashboard_test.clj | 45 ++++++++++++++++--- 8 files changed, 133 insertions(+), 50 deletions(-) diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj index 6852cb2de51..d6bf784eb12 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj @@ -5,6 +5,7 @@ [metabase-enterprise.test :as met] [metabase.api.dashboard-test :as api.dashboard-test] [metabase.models :refer [Card Dashboard DashboardCard FieldValues]] + [metabase.models.params.chain-filter] [metabase.models.params.chain-filter-test :as chain-filter-test] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] @@ -18,16 +19,19 @@ (met/with-gtaps {:gtaps {:categories {:query (mt/mbql-query categories {:filter [:< $id 3]})}}} (mt/with-temp-vals-in-db FieldValues (u/the-id (db/select-one-id FieldValues :field_id (mt/id :categories :name))) {:values ["Good" "Bad"]} (api.dashboard-test/with-chain-filter-fixtures [{:keys [dashboard]}] - (testing "GET /api/dashboard/:id/params/:param-key/values" - (api.dashboard-test/let-url [url (api.dashboard-test/chain-filter-values-url dashboard "_CATEGORY_NAME_")] - (is (= {:values ["African" "American"] - :has_more_values false} - (chain-filter-test/take-n-values 2 (mt/user-http-request :rasta :get 200 url)))))) - (testing "GET /api/dashboard/:id/params/:param-key/search/:query" - (api.dashboard-test/let-url [url (api.dashboard-test/chain-filter-search-url dashboard "_CATEGORY_NAME_" "a")] - (is (= {:values ["African" "American"] - :has_more_values false} - (mt/user-http-request :rasta :get 200 url)))))))))) + (with-redefs [metabase.models.params.chain-filter/use-cached-field-values? (constantly false)] + (testing "GET /api/dashboard/:id/params/:param-key/values" + (api.dashboard-test/let-url [url (api.dashboard-test/chain-filter-values-url dashboard "_CATEGORY_NAME_")] + (is (= {:values ["African" "American"] + :has_more_values false} + (->> url + (mt/user-http-request :rasta :get 200) + (chain-filter-test/take-n-values 2)))))) + (testing "GET /api/dashboard/:id/params/:param-key/search/:query" + (api.dashboard-test/let-url [url (api.dashboard-test/chain-filter-search-url dashboard "_CATEGORY_NAME_" "a")] + (is (= {:values ["African" "American"] + :has_more_values false} + (mt/user-http-request :rasta :get 200 url))))))))))) (deftest add-card-parameter-mapping-permissions-test (testing "POST /api/dashboard/:id/cards" diff --git a/frontend/test/metabase/scenarios/dashboard-filters/reproductions/16112-nodata-should-use-dashboard-filters.cy.spec.js b/frontend/test/metabase/scenarios/dashboard-filters/reproductions/16112-nodata-should-use-dashboard-filters.cy.spec.js index 0890d5659b2..333f0e0f966 100644 --- a/frontend/test/metabase/scenarios/dashboard-filters/reproductions/16112-nodata-should-use-dashboard-filters.cy.spec.js +++ b/frontend/test/metabase/scenarios/dashboard-filters/reproductions/16112-nodata-should-use-dashboard-filters.cy.spec.js @@ -72,23 +72,39 @@ describe("issues 15119 and 16112", () => { ], }); + // Actually need to setup the linked filter: + visitDashboard(dashboard_id); + cy.get('[data-metabase-event="Dashboard;Edit"]').click(); + cy.findByText("Rating Filter").click(); + cy.findByText("Linked filters").click(); + // cy.findByText("Reviewer Filter").click(); + cy.findByText("Limit this filter's choices") + .parent() + .within(() => { + // turn on the toggle + cy.get("input").click(); + }); + cy.findByText("Save").click(); + cy.signIn("nodata"); visitDashboard(dashboard_id); }, ); - cy.findByText(ratingFilter.name).click(); - popover().contains("3").click(); + cy.findByText(reviewerFilter.name).click(); + popover().contains("adam").click(); cy.button("Add filter").click(); - cy.get(".DashCard").should("contain", "maia").and("contain", "daryl"); - cy.location("search").should("eq", "?rating=3"); + cy.get(".DashCard").should("contain", "adam"); + cy.location("search").should("eq", "?reviewer=adam"); - cy.findByText(reviewerFilter.name).click(); - cy.findByPlaceholderText("Enter some text").type("maia{enter}").blur(); + cy.findByText(ratingFilter.name).click(); + + popover().contains("5").click(); cy.button("Add filter").click(); - cy.get(".DashCard").should("contain", "maia").and("not.contain", "daryl"); - cy.location("search").should("eq", "?reviewer=maia&rating=3"); + cy.get(".DashCard").should("contain", "adam"); + cy.get(".DashCard").should("contain", "5"); + cy.location("search").should("eq", "?reviewer=adam&rating=5"); }); }); diff --git a/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js index 11b9e4c2db2..07de30a9108 100644 --- a/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js +++ b/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js @@ -58,6 +58,22 @@ describe("support > permissions (metabase#8472)", () => { it("should allow a nodata user to select the filter", () => { cy.signIn("nodata"); - filterDashboard(false); + filterDashboard(); + }); + + it("should not allow a nocollection user to visit the page, hence cannot see the filter", () => { + cy.server(); + cy.route("GET", "/api/dashboard/1/params/search/100 Main Street").as( + "search", + ); + + cy.signIn("nocollection"); + cy.request({ + method: "GET", + url: "/api/dashboard/1", + failOnStatusCode: false, + }).should(xhr => { + expect(xhr.status).to.equal(403); + }); }); }); diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj index bf751f2278c..4a36918fba4 100644 --- a/src/metabase/api/dashboard.clj +++ b/src/metabase/api/dashboard.clj @@ -31,12 +31,15 @@ [metabase.query-processor.dashboard :as qp.dashboard] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.constraints :as qp.constraints] + [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.pivot :as qp.pivot] [metabase.query-processor.util :as qp.util] [metabase.related :as related] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] + [metabase.util.malli :as mu] + [metabase.util.malli.schema :as ms] [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db] @@ -716,25 +719,17 @@ field-id (param-key->field-ids dashboard param-key)] [field-id value]))) -(s/defn chain-filter +(mu/defn chain-filter "C H A I N filters! - ;; show me categories - (chain-filter 62 \"ee876336\" {}) - ;; -> {:values (\"African\" \"American\" \"Artisan\" ...) - :has_more_values false} - - ;; show me categories that have expensive restaurants - (chain-filter 62 \"ee876336\" {\"6f10a41f\" 4}) - ;; -> {:values (\"Japanese\" \"Steakhouse\") - :has_more_values false}" + Used to query for values that populate chained filter dropdowns and text search boxes." ([dashboard param-key constraint-param-key->value] (chain-filter dashboard param-key constraint-param-key->value nil)) - ([dashboard :- su/Map - param-key :- su/NonBlankString - constraint-param-key->value :- su/Map - query :- (s/maybe su/NonBlankString)] + ([dashboard :- ms/Map + param-key :- ms/NonBlankString + constraint-param-key->value :- ms/Map + query :- [:maybe ms/NonBlankString]] (let [constraints (chain-filter-constraints dashboard constraint-param-key->value) field-ids (param-key->field-ids dashboard param-key)] (when (empty? field-ids) @@ -750,7 +745,7 @@ field-ids) values (distinct (mapcat :values results)) has_more_values (boolean (some true? (map :has_more_values results)))] - ;; results can come back as [v ...] *or* as [[orig remapped] ...]. Sort by remapped value if that's the case + ;; results can come back as [v ...] *or* as [[orig remapped] ...]. Sort by remapped value if it's there {:values (if (sequential? (first values)) (sort-by second values) (sort values)) @@ -791,7 +786,9 @@ GET /api/dashboard/1/params/abc/values?def=100" [id param-key :as {:keys [query-params]}] (let [dashboard (api/read-check Dashboard id)] - (param-values dashboard param-key query-params))) + ;; If a user can read the dashboard, then they can lookup filters. This also works with sandboxing. + (binding [qp.perms/*param-values-query* true] + (param-values dashboard param-key query-params)))) #_{:clj-kondo/ignore [:deprecated-var]} (api/defendpoint-schema GET "/:id/params/:param-key/search/:query" @@ -805,7 +802,9 @@ Currently limited to first 1000 results." [id param-key query :as {:keys [query-params]}] (let [dashboard (api/read-check Dashboard id)] - (param-values dashboard param-key query-params query))) + ;; If a user can read the dashboard, then they can lookup filters. This also works with sandboxing. + (binding [qp.perms/*param-values-query* true] + (param-values dashboard param-key query-params query)))) #_{:clj-kondo/ignore [:deprecated-var]} (api/defendpoint-schema GET "/params/valid-filter-fields" diff --git a/src/metabase/api/field.clj b/src/metabase/api/field.clj index 967c162abe7..45178969c52 100644 --- a/src/metabase/api/field.clj +++ b/src/metabase/api/field.clj @@ -247,8 +247,7 @@ :field_id field-id}))) ;; TODO -- not sure `has_field_values` actually has to be `:list` -- see code above. -#_{:clj-kondo/ignore [:deprecated-var]} -(api/defendpoint-schema GET "/:id/values" +(api/defendpoint GET "/:id/values" "If a Field's value of `has_field_values` is `:list`, return a list of all the distinct values of the Field, and (if defined by a User) a map of human-readable remapped values." [id] diff --git a/src/metabase/models/params/custom_values.clj b/src/metabase/models/params/custom_values.clj index 90f9c9a0126..99348baa9fc 100644 --- a/src/metabase/models/params/custom_values.clj +++ b/src/metabase/models/params/custom_values.clj @@ -116,10 +116,10 @@ (defn parameter->values "Given a parameter with a custom-values source, return the values. - `default-case-fn` is a 0-arity function that returns values list when: + `default-case-thunk` is a 0-arity function that returns values list when: - :values_source_type = card but the card is archived or the card no longer contains the value-field. - :values_source_type = nil." - [parameter query default-case-fn] + [parameter query default-case-thunk] (case (:values_source_type parameter) "static-list" (static-list-values parameter query) "card" (let [card (db/select-one Card :id (get-in parameter [:values_source_config :card_id]))] @@ -127,8 +127,8 @@ (throw (ex-info "You don't have permissions to do that." {:status-code 403}))) (if (can-get-card-values? card (get-in parameter [:values_source_config :value_field])) (card-values parameter query) - (default-case-fn))) - nil (default-case-fn) + (default-case-thunk))) + nil (default-case-thunk) (throw (ex-info (tru "Invalid parameter source {0}" (:values_source_type parameter)) {:status-code 400 :parameter parameter})))) diff --git a/src/metabase/query_processor/middleware/permissions.clj b/src/metabase/query_processor/middleware/permissions.clj index 18da7350991..08ce2aaaf25 100644 --- a/src/metabase/query_processor/middleware/permissions.clj +++ b/src/metabase/query_processor/middleware/permissions.clj @@ -15,6 +15,7 @@ [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] + [metabase.util.malli :as mu] [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db])) @@ -89,16 +90,29 @@ (doseq [{query :dataset_query} (qp.u.tag-referenced-cards/tags-referenced-cards outer-query)] (check-query-permissions* query))) -(s/defn ^:private check-query-permissions* +(def ^:dynamic *param-values-query* + "Used to allow users looking at a dashboard to view (possibly chained) filters." + false) + +(mu/defn ^:private check-query-permissions* "Check that User with `user-id` has permissions to run `query`, or throw an exception." - [outer-query :- su/Map] + [outer-query :- :map] (when *current-user-id* (log/tracef "Checking query permissions. Current user perms set = %s" (pr-str @*current-user-permissions-set*)) - (if *card-id* + (cond + *card-id* (do (check-card-read-perms *card-id*) (when-not (has-data-perms? (required-perms outer-query)) (check-block-permissions outer-query))) + + ;; set when querying for field values of dashboard filters, which only require + ;; collection perms for the dashboard and not ad-hoc query perms + *param-values-query* + (when-not (has-data-perms? (required-perms outer-query)) + (check-block-permissions outer-query)) + + :else (check-ad-hoc-query-perms outer-query)))) (defn check-query-permissions diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj index 7930a3b2755..cd396c39e38 100644 --- a/test/metabase/api/dashboard_test.clj +++ b/test/metabase/api/dashboard_test.clj @@ -31,6 +31,7 @@ [metabase.models.dashboard-test :as dashboard-test] [metabase.models.field-values :as field-values] [metabase.models.interface :as mi] + [metabase.models.params.chain-filter :as chain-filter] [metabase.models.params.chain-filter-test :as chain-filter-test] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] @@ -39,6 +40,7 @@ [metabase.public-settings.premium-features-test :as premium-features-test] [metabase.query-processor :as qp] + [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.streaming.test-util :as streaming.test-util] [metabase.server.middleware.util :as mw.util] [metabase.test :as mt] @@ -1098,6 +1100,7 @@ (deftest update-cards-for-copy-test (testing "When copy style is shallow returns original ordered-cards" + (let [ordered-cards [{:card_id 1 :card {:id 1} :series [{:id 2}]} {:card_id 3 :card {:id 3}}]] (is (= ordered-cards @@ -1941,6 +1944,24 @@ query) query-params)) +(deftest dashboard-chain-filter-permissions-test + (with-chain-filter-fixtures [{:keys [dashboard param-keys]}] + (let [url (chain-filter-values-url dashboard (:category-name param-keys))] + (testing (str "\nGET /api/" url "\n") + (testing "\nShow me names of categories that have expensive venues (price = 4), while I lack permisisons." + (with-redefs [metabase.models.params.chain-filter/use-cached-field-values? (constantly false)] + (binding [qp.perms/*card-id* nil] ;; this situation was observed when running constrained chain filters. + (is (= {:values ["African" "American" "Artisan" "Asian"] :has_more_values false} + (chain-filter-test/take-n-values 4 (mt/user-http-request :rasta :get 200 url))))))))) + + (let [url (chain-filter-values-url dashboard (:category-name param-keys) (:price param-keys) 4)] + (testing (str "\nGET /api/" url "\n") + (testing "\nShow me names of categories that have expensive venues (price = 4), while I lack permisisons." + (with-redefs [chain-filter/use-cached-field-values? (constantly false)] + (binding [qp.perms/*card-id* nil] + (is (= {:values ["Japanese" "Steakhouse"], :has_more_values false} + (chain-filter-test/take-n-values 3 (mt/user-http-request :rasta :get 200 url))))))))))) + (deftest dashboard-chain-filter-test (testing "GET /api/dashboard/:id/params/:param-key/values" (with-chain-filter-fixtures [{:keys [dashboard param-keys]}] @@ -1983,19 +2004,33 @@ (assoc :card_id (:id card-2)))]] (is (= {:values ["African" "American" "Artisan"] :has_more_values false} - (chain-filter-test/take-n-values 3 (mt/user-http-request :rasta :get 200 (chain-filter-values-url - (:id dashboard) - (:category-name param-keys))))))))) + (->> (chain-filter-values-url (:id dashboard) (:category-name param-keys)) + (mt/user-http-request :rasta :get 200) + (chain-filter-test/take-n-values 3))))))) + (testing "should check perms for the Fields in question" (mt/with-temp-copy-of-db (with-chain-filter-fixtures [{:keys [dashboard param-keys]}] (perms/revoke-data-perms! (perms-group/all-users) (mt/id)) + ;; HACK: we currently 403 on chain-filter calls that require running a MBQL ;; but 200 on calls that we could just use the cache. ;; It's not ideal and we definitely need to have a consistent behavior (with-redefs [field-values/field-should-have-field-values? (fn [_] false)] - (is (= "You don't have permissions to do that." - (mt/user-http-request :rasta :get 403 (chain-filter-values-url (:id dashboard) (:category-name param-keys))))))))))) + (is (= {:values ["African" "American" "Artisan"] + :has_more_values false} + (->> (chain-filter-values-url (:id dashboard) (:category-name param-keys)) + (mt/user-http-request :rasta :get 200) + (chain-filter-test/take-n-values 3)))))))) + + (testing "missing data perms should not affect perms for the Fields in question when users have collection access" + (mt/with-temp-copy-of-db + (with-chain-filter-fixtures [{:keys [dashboard param-keys]}] + (perms/revoke-data-perms! (perms-group/all-users) (mt/id)) + (is (= {:values ["African" "American" "Artisan"] :has_more_values false} + (->> (chain-filter-values-url (:id dashboard) (:category-name param-keys)) + (mt/user-http-request :rasta :get 200) + (chain-filter-test/take-n-values 3))))))))) (deftest dashboard-with-static-list-parameters-test (testing "A dashboard that has parameters that has static values" -- GitLab