From da106c7217c244e3d25a429f68fefbdec2f04161 Mon Sep 17 00:00:00 2001
From: Noah Moss <32746338+noahmoss@users.noreply.github.com>
Date: Fri, 6 Sep 2024 17:14:05 -0400
Subject: [PATCH] Fix admin override for public actions (#47717)

---
 src/metabase/api/public.clj                   |   4 +-
 test/metabase/api/dashboard_test.clj          |   8 +-
 test/metabase/api/public_test.clj             | 126 +++++++++---------
 .../metabase/driver/sql_jdbc/actions_test.clj |   3 +-
 test/metabase/query_processor/card_test.clj   |   3 +-
 .../query_processor/dashboard_test.clj        |   3 +-
 6 files changed, 73 insertions(+), 74 deletions(-)

diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj
index 90b03e67b0a..70fd0978f70 100644
--- a/src/metabase/api/public.clj
+++ b/src/metabase/api/public.clj
@@ -363,7 +363,7 @@
           ;; Run this query with full superuser perms. We don't want the various perms checks
           ;; failing because there are no current user perms; if this Dashcard is public
           ;; you're by definition allowed to run it without a perms check anyway
-          (binding [api/*current-user-permissions-set* (delay #{"/"})]
+          (mw.session/as-admin
             ;; Undo middleware string->keyword coercion
             (actions/execute-dashcard! dashboard-id dashcard-id (update-keys parameters name))))))))
 
@@ -679,7 +679,7 @@
         ;; Run this query with full superuser perms. We don't want the various perms checks
         ;; failing because there are no current user perms; if this Dashcard is public
         ;; you're by definition allowed to run it without a perms check anyway
-        (binding [api/*current-user-permissions-set* (delay #{"/"})]
+        (mw.session/as-admin
           (let [action (api/check-404 (action/select-action :public_uuid uuid :archived false))]
             (snowplow/track-event! ::snowplow/action
                                    {:event     :action-executed
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index de4404f4541..0ce88898284 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -322,7 +322,7 @@
 (deftest get-dashboard-test
   (mt/dataset test-data
     (mt/with-column-remappings [orders.user_id people.name]
-      (binding [api/*current-user-permissions-set* (atom #{"/"})]
+      (mt/as-admin
         (t2.with-temp/with-temp
           [Dashboard {dashboard-id :id} {:name             "Test Dashboard"
                                          :creator_id       (mt/user->id :crowberto)
@@ -393,7 +393,7 @@
 (deftest last-used-parameter-value-test
   (mt/dataset test-data
     (mt/with-column-remappings [orders.user_id people.name]
-      (binding [api/*current-user-permissions-set* (atom #{"/"})]
+      (mt/as-admin
         (t2.with-temp/with-temp
           [Dashboard {dashboard-a-id :id} {:name       "Test Dashboard"
                                            :creator_id (mt/user->id :crowberto)
@@ -422,7 +422,7 @@
                                               :card_id            card-id
                                               :dashboard_id       dashboard-b-id}]
           (testing "User's set parameter is saved and sent back in the dashboard response, unique per dashboard."
-            ;; api request mimicking a user setting a parameter value
+           ;; api request mimicking a user setting a parameter value
             (is (some? (mt/user-http-request :rasta :post (format "dashboard/%d/dashcard/%s/card/%s/query" dashboard-a-id dashcard-a-id card-id)
                                              {:parameters [{:id "a" :value ["initial value"]}]})))
             (is (some? (mt/user-http-request :rasta :post (format "dashboard/%d/dashcard/%s/card/%s/query" dashboard-b-id dashcard-b-id card-id)
@@ -434,7 +434,7 @@
                    {:dashboard-a (:last_used_param_values (mt/user-http-request :rasta :get 200 (format "dashboard/%d" dashboard-a-id)))
                     :dashboard-b (:last_used_param_values (mt/user-http-request :rasta :get 200 (format "dashboard/%d" dashboard-b-id)))})))
           (testing "If a User unsets a parameter's value, the default is NOT used."
-            ;; api request mimicking a user clearing parameter value, and no default exists
+           ;; api request mimicking a user clearing parameter value, and no default exists
             (is (some? (mt/user-http-request :rasta :post (format "dashboard/%d/dashcard/%s/card/%s/query" dashboard-a-id dashcard-a-id card-id)
                                              {:parameters [{:id "a" :value nil}]})))
             (is (= {}
diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj
index ee0324f17f5..895db4a2714 100644
--- a/test/metabase/api/public_test.clj
+++ b/test/metabase/api/public_test.clj
@@ -1757,68 +1757,70 @@
 ;;; --------------------------------- POST /api/public/action/:uuid/execute ----------------------------------
 
 (deftest execute-public-action-test
-  (mt/with-actions-test-data-and-actions-enabled
-    (mt/with-temporary-setting-values [enable-public-sharing true]
-      (let [{:keys [public_uuid] :as action-opts} (shared-obj)]
-        (mt/with-actions [{} action-opts]
-          ;; Decrease the throttle threshold to 1 so we can test the throttle,
-          ;; and set the throttle delay high enough the throttle will definitely trigger
-          (with-redefs [api.public/action-execution-throttle (throttle/make-throttler :action-uuid :attempts-threshold 1 :initial-delay-ms 20000)]
-            (testing "Happy path - we can execute a public action"
-              (is (=? {:rows-affected 1}
-                      (client/client
-                       :post 200
-                       (format "public/action/%s/execute" public_uuid)
-                       {:parameters {:id 1 :name "European"}}))))
-            (testing "Test throttle"
-              (let [throttled-response (client/client-full-response
-                                        :post 429
-                                        (format "public/action/%s/execute" public_uuid)
-                                        {:parameters {:id 1 :name "European"}})]
-                (is (str/starts-with? (:body throttled-response) "Too many attempts!"))
-                (is (contains? (:headers throttled-response) "Retry-After"))))))
-        ;; Lift the throttle attempts threshold so we don't have to wait between requests
-        (with-redefs [api.public/action-execution-throttle (throttle/make-throttler :action-uuid :attempts-threshold 1000)]
-          (mt/with-actions [{} (assoc action-opts :archived true)]
-            (testing "Check that we get a 400 if the action is archived"
-              (is (= "Not found."
-                     (client/client
-                      :post 404
-                      (format "public/action/%s/execute" (str (random-uuid)))
-                      {:parameters {:id 1 :name "European"}})))))
-          (mt/with-actions [{} action-opts]
-            (testing "Check that we get a 400 if the action doesn't exist"
-              (is (= "Not found."
-                     (client/client
-                      :post 404
-                      (format "public/action/%s/execute" (str (random-uuid)))
-                      {:parameters {:id 1 :name "European"}}))))
-            (testing "Check that we get a 400 if sharing is disabled."
-              (mt/with-temporary-setting-values [enable-public-sharing false]
-                (is (= "An error occurred."
-                       (client/client
-                        :post 400
-                        (format "public/action/%s/execute" public_uuid)
-                        {:parameters {:id 1 :name "European"}})))))
-            (testing "Check that we get a 400 if actions are disabled for the database."
-              (mt/with-temp-vals-in-db Database (mt/id) {:settings {:database-enable-actions false}}
-                (is (= "An error occurred."
-                       (client/client
-                        :post 400
-                        (format "public/action/%s/execute" public_uuid)
-                        {:parameters {:id 1 :name "European"}})))))
-            (testing "Check that we send a snowplow event when execute an action"
-              (snowplow-test/with-fake-snowplow-collector
-                (client/client
-                 :post 200
-                 (format "public/action/%s/execute" public_uuid)
-                 {:parameters {:id 1 :name "European"}})
-                (is (= {:data   {"action_id" (t2/select-one-pk 'Action :public_uuid public_uuid)
-                                 "event"     "action_executed"
-                                 "source"    "public_form"
-                                 "type"      "query"}
-                        :user-id nil}
-                       (last (snowplow-test/pop-event-data-and-user-id!))))))))))))
+  (mt/with-premium-features #{:advanced-permissions}
+    (mt/with-actions-test-data-and-actions-enabled
+      (mt/with-no-data-perms-for-all-users!
+        (mt/with-temporary-setting-values [enable-public-sharing true]
+          (let [{:keys [public_uuid] :as action-opts} (shared-obj)]
+            (mt/with-actions [{} action-opts]
+              ;; Decrease the throttle threshold to 1 so we can test the throttle,
+              ;; and set the throttle delay high enough the throttle will definitely trigger
+              (with-redefs [api.public/action-execution-throttle (throttle/make-throttler :action-uuid :attempts-threshold 1 :initial-delay-ms 20000)]
+                (testing "Happy path - we can execute a public action"
+                  (is (=? {:rows-affected 1}
+                          (client/client
+                           :post 200
+                           (format "public/action/%s/execute" public_uuid)
+                           {:parameters {:id 1 :name "European"}}))))
+                (testing "Test throttle"
+                  (let [throttled-response (client/client-full-response
+                                            :post 429
+                                            (format "public/action/%s/execute" public_uuid)
+                                            {:parameters {:id 1 :name "European"}})]
+                    (is (str/starts-with? (:body throttled-response) "Too many attempts!"))
+                    (is (contains? (:headers throttled-response) "Retry-After"))))))
+            ;; Lift the throttle attempts threshold so we don't have to wait between requests
+            (with-redefs [api.public/action-execution-throttle (throttle/make-throttler :action-uuid :attempts-threshold 1000)]
+              (mt/with-actions [{} (assoc action-opts :archived true)]
+                (testing "Check that we get a 400 if the action is archived"
+                  (is (= "Not found."
+                         (client/client
+                          :post 404
+                          (format "public/action/%s/execute" (str (random-uuid)))
+                          {:parameters {:id 1 :name "European"}})))))
+              (mt/with-actions [{} action-opts]
+                (testing "Check that we get a 400 if the action doesn't exist"
+                  (is (= "Not found."
+                         (client/client
+                          :post 404
+                          (format "public/action/%s/execute" (str (random-uuid)))
+                          {:parameters {:id 1 :name "European"}}))))
+                (testing "Check that we get a 400 if sharing is disabled."
+                  (mt/with-temporary-setting-values [enable-public-sharing false]
+                    (is (= "An error occurred."
+                           (client/client
+                            :post 400
+                            (format "public/action/%s/execute" public_uuid)
+                            {:parameters {:id 1 :name "European"}})))))
+                (testing "Check that we get a 400 if actions are disabled for the database."
+                  (mt/with-temp-vals-in-db Database (mt/id) {:settings {:database-enable-actions false}}
+                    (is (= "An error occurred."
+                           (client/client
+                            :post 400
+                            (format "public/action/%s/execute" public_uuid)
+                            {:parameters {:id 1 :name "European"}})))))
+                (testing "Check that we send a snowplow event when execute an action"
+                  (snowplow-test/with-fake-snowplow-collector
+                    (client/client
+                     :post 200
+                     (format "public/action/%s/execute" public_uuid)
+                     {:parameters {:id 1 :name "European"}})
+                    (is (= {:data   {"action_id" (t2/select-one-pk 'Action :public_uuid public_uuid)
+                                     "event"     "action_executed"
+                                     "source"    "public_form"
+                                     "type"      "query"}
+                            :user-id nil}
+                           (last (snowplow-test/pop-event-data-and-user-id!))))))))))))))
 
 (deftest format-export-middleware-test
   (mt/with-temporary-setting-values [enable-public-sharing true]
diff --git a/test/metabase/driver/sql_jdbc/actions_test.clj b/test/metabase/driver/sql_jdbc/actions_test.clj
index 536785c72c4..631b1f45837 100644
--- a/test/metabase/driver/sql_jdbc/actions_test.clj
+++ b/test/metabase/driver/sql_jdbc/actions_test.clj
@@ -5,7 +5,6 @@
    [clojure.test :refer :all]
    [metabase.actions :as actions]
    [metabase.actions.error :as actions.error]
-   [metabase.api.common :refer [*current-user-permissions-set*]]
    [metabase.driver :as driver]
    [metabase.driver.sql-jdbc.actions :as sql-jdbc.actions]
    [metabase.lib.schema.actions :as lib.schema.actions]
@@ -57,7 +56,7 @@
           (reset! parse-sql-error-called? false)
           ;; attempting to delete the `Pizza` category should fail because there are several rows in `venues` that have
           ;; this `category_id` -- it's an FK constraint violation.
-          (binding [*current-user-permissions-set* (delay #{"/"})]
+          (mt/as-admin
             (is (thrown-with-msg? Exception #"Referential integrity constraint violation:.*"
                                   (actions/perform-action! :row/delete (mt/mbql-query categories {:filter [:= $id 58]})))))
           (testing "Make sure our impl was actually called."
diff --git a/test/metabase/query_processor/card_test.clj b/test/metabase/query_processor/card_test.clj
index eb1ea6676e1..a19fefce31e 100644
--- a/test/metabase/query_processor/card_test.clj
+++ b/test/metabase/query_processor/card_test.clj
@@ -3,7 +3,6 @@
   (:require
    [cheshire.core :as json]
    [clojure.test :refer :all]
-   [metabase.api.common :as api]
    [metabase.models :refer [Card]]
    [metabase.models.data-permissions :as data-perms]
    [metabase.models.interface :as mi]
@@ -20,7 +19,7 @@
   [card-id]
   ;; TODO -- we shouldn't do the perms checks if there is no current User context. It seems like API-level perms check
   ;; stuff doesn't belong in the Dashboard QP namespace
-  (binding [api/*current-user-permissions-set* (atom #{"/"})]
+  (mt/as-admin
     (qp.card/process-query-for-card
      card-id :api
      :make-run (constantly
diff --git a/test/metabase/query_processor/dashboard_test.clj b/test/metabase/query_processor/dashboard_test.clj
index 68e5d54b552..3562caa95bc 100644
--- a/test/metabase/query_processor/dashboard_test.clj
+++ b/test/metabase/query_processor/dashboard_test.clj
@@ -2,7 +2,6 @@
   "There are more e2e tests in [[metabase.api.dashboard-test]]."
   (:require
    [clojure.test :refer :all]
-   [metabase.api.common :as api]
    [metabase.api.dashboard-test :as api.dashboard-test]
    [metabase.models
     :refer [Card Dashboard DashboardCard DashboardCardSeries]]
@@ -17,7 +16,7 @@
 (defn- run-query-for-dashcard [dashboard-id card-id dashcard-id & options]
   ;; TODO -- we shouldn't do the perms checks if there is no current User context. It seems like API-level perms check
   ;; stuff doesn't belong in the Dashboard QP namespace
-  (binding [api/*current-user-permissions-set* (atom #{"/"})]
+  (mt/as-admin
     (apply qp.dashboard/process-query-for-dashcard
            :dashboard-id dashboard-id
            :card-id      card-id
-- 
GitLab