From f6cf82df4827d59093d6b785495064a09f5269d0 Mon Sep 17 00:00:00 2001 From: Cam Saul <cammsaul@gmail.com> Date: Wed, 8 May 2019 21:06:20 -0700 Subject: [PATCH] Properly clone/restore thread binding frames in async QP :frame_with_picture: --- .../query_processor/middleware/async_wait.clj | 19 ++++--- test/metabase/api/dataset_test.clj | 30 ++++++++++-- .../middleware/async_wait_test.clj | 49 +++++++++++++++++++ .../middleware/permissions_test.clj | 29 +++++++++-- 4 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 test/metabase/query_processor/middleware/async_wait_test.clj diff --git a/src/metabase/query_processor/middleware/async_wait.clj b/src/metabase/query_processor/middleware/async_wait.clj index 6d6666a3161..4b8bbcc0c63 100644 --- a/src/metabase/query_processor/middleware/async_wait.clj +++ b/src/metabase/query_processor/middleware/async_wait.clj @@ -13,7 +13,8 @@ [metabase.util :as u] [metabase.util.i18n :refer [trs]] [schema.core :as s]) - (:import [java.util.concurrent Executors ExecutorService])) + (:import clojure.lang.Var + [java.util.concurrent Executors ExecutorService])) (defsetting max-simultaneous-queries-per-db (trs "Maximum number of simultaneous queries to allow per connected Database.") @@ -60,12 +61,16 @@ (def ^:private ^:dynamic *already-in-thread-pool?* false) (defn- runnable ^Runnable [qp query respond raise canceled-chan] - (fn [] - (binding [*already-in-thread-pool?* true] - (try - (qp query respond raise canceled-chan) - (catch Throwable e - (raise e)))))) + ;; stash & restore bound dynamic vars. This is how Clojure does it for futures and the like in `binding-conveyor-fn` + ;; (see source for `future` or `future-call`) which is unfortunately private + (let [frame (Var/cloneThreadBindingFrame)] + (^:once fn* [] + (Var/resetThreadBindingFrame frame) + (binding [*already-in-thread-pool?* true] + (try + (qp query respond raise canceled-chan) + (catch Throwable e + (raise e))))))) (defn- run-in-thread-pool [qp {database-id :database, :as query} respond raise canceled-chan] (try diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj index c25a1717e3d..0864353fddc 100644 --- a/test/metabase/api/dataset_test.clj +++ b/test/metabase/api/dataset_test.clj @@ -9,16 +9,23 @@ [medley.core :as m] [metabase.models [card :refer [Card]] - [database :as database] - [query-execution :refer [QueryExecution]]] + [database :as database :refer [Database]] + [field :refer [Field]] + [permissions :as perms] + [permissions-group :as group] + [query-execution :refer [QueryExecution]] + [table :refer [Table]]] [metabase.query-processor.middleware.constraints :as constraints] - [metabase.test.data :as data] + [metabase.test + [data :as data] + [util :as tu]] [metabase.test.data [dataset-definitions :as defs] [datasets :refer [expect-with-driver]] [users :as test-users]] [metabase.test.util.log :as tu.log] [metabase.util :as u] + [schema.core :as s] [toucan.db :as db] [toucan.util.test :as tt]) (:import com.fasterxml.jackson.core.JsonGenerator)) @@ -263,3 +270,20 @@ :type :query :query {:source-table (data/id :venues)}})] (or row-count result)))) + +;; make sure `POST /dataset` calls check user permissions +(tu/expect-schema + {:status (s/eq "failed") + :error (s/eq "You do not have permissions to run this query.") + s/Keyword s/Any} + (tt/with-temp* [Database [{db-id :id} (select-keys (data/db) [:engine :details])] + Table [{table-id :id} {:db_id db-id, :name "VENUES"}] + Field [_ {:table_id table-id, :name "ID"}]] + ;; give all-users *partial* permissions for the DB, so we know we're checking more than just read permissions for + ;; the Database + (perms/revoke-permissions! (group/all-users) db-id) + (perms/grant-permissions! (group/all-users) db-id "schema_that_does_not_exist") + ((test-users/user->client :rasta) :post "dataset" + {:database db-id + :type :query + :query {:source-table table-id, :limit 1}}))) diff --git a/test/metabase/query_processor/middleware/async_wait_test.clj b/test/metabase/query_processor/middleware/async_wait_test.clj new file mode 100644 index 00000000000..e6f71215fd6 --- /dev/null +++ b/test/metabase/query_processor/middleware/async_wait_test.clj @@ -0,0 +1,49 @@ +(ns metabase.query-processor.middleware.async-wait-test + (:require [clojure.core.async :as a] + [expectations :refer [expect]] + [metabase.models.database :refer [Database]] + [metabase.query-processor.middleware.async-wait :as async-wait] + [metabase.test.util.async :as tu.async] + [metabase.util :as u] + [toucan.util.test :as tt]) + (:import java.util.concurrent.Executors)) + +(def ^:private ^:dynamic *dynamic-var* false) + +(defn- async-wait-bound-value + "Check the bound value of `*dynamic-var*` when a function is executed after the `async-wait` using the thread pool for + Database with `db-id`." + [db-id] + (let [bound-value (promise)] + (tu.async/with-open-channels [canceled-chan (a/promise-chan)] + ((async-wait/wait-for-turn + (fn [& _] + (deliver bound-value *dynamic-var*))) + {:database db-id} + identity + identity + canceled-chan)) + (u/deref-with-timeout bound-value 1000))) + +;; basic sanity check: bound value of `*dynamic-var*` should be `false` +(expect + false + (tt/with-temp Database [{db-id :id}] + (async-wait-bound-value db-id))) + +;; bound dynamic vars should get re-bound by in the async wait fn +(expect + ::wow + (tt/with-temp Database [{db-id :id}] + (binding [*dynamic-var* ::wow] + (async-wait-bound-value db-id)))) + +;; binding should not be persisted between executions -- should be reset when we reuse a thread +(expect + false + (let [thread-pool (Executors/newSingleThreadExecutor)] + (with-redefs [async-wait/db-thread-pool (constantly thread-pool)] + (tt/with-temp Database [{db-id :id}] + (binding [*dynamic-var* true] + (async-wait-bound-value db-id)) + (async-wait-bound-value db-id))))) diff --git a/test/metabase/query_processor/middleware/permissions_test.clj b/test/metabase/query_processor/middleware/permissions_test.clj index 717a10a054e..f06e93de664 100644 --- a/test/metabase/query_processor/middleware/permissions_test.clj +++ b/test/metabase/query_processor/middleware/permissions_test.clj @@ -1,25 +1,32 @@ (ns metabase.query-processor.middleware.permissions-test "Tests for the middleware that checks whether the current user has permissions to run a given query." (:require [expectations :refer :all] + [metabase + [query-processor :as qp] + [util :as u]] + [metabase.api.common :as api] [metabase.models [database :refer [Database]] [permissions :as perms] [permissions-group :as perms-group] [table :refer [Table]]] [metabase.query-processor.middleware.permissions :refer [check-query-permissions]] + [metabase.test + [data :as data] + [util :as tu]] [metabase.test.data.users :as users] - [metabase.util :as u] + [schema.core :as s] [toucan.util.test :as tt])) (def ^:private ^{:arglists '([query]), :style/indent 0} check-perms (check-query-permissions identity)) (defn- do-with-rasta - "Call F with rasta as the current user." + "Call `f` with Rasta as the current user." [f] (users/do-with-test-user :rasta f)) (defn- check-perms-for-rasta - "Check permissions for QUERY with rasta as the current user." + "Check permissions for `query` with rasta as the current user." {:style/indent 0} [query] (do-with-rasta (fn [] (check-perms query)))) @@ -113,3 +120,19 @@ {:database (u/get-id db) :type :query :query {:source-query {:source-table (u/get-id table)}}})) + + +;; Make sure it works end-to-end: make sure bound `*current-user-id*` and `*current-user-permissions-set*` are used to +;; permissions check queries +(tu/expect-schema + {:status (s/eq :failed) + :class (s/eq Exception) + :error (s/eq "You do not have permissions to run this query.") + s/Keyword s/Any} + (binding [api/*current-user-id* (users/user->id :rasta) + api/*current-user-permissions-set* (delay #{})] + (qp/process-query + {:database (data/id) + :type :query + :query {:source-table (data/id :venues) + :limit 1}}))) -- GitLab